<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zachary Sturman</title>
    <description>The latest articles on DEV Community by Zachary Sturman (@zacharysturman).</description>
    <link>https://dev.to/zacharysturman</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%2F1910384%2Feef7a207-654e-4386-81c9-1041c6b4b07a.jpeg</url>
      <title>DEV Community: Zachary Sturman</title>
      <link>https://dev.to/zacharysturman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zacharysturman"/>
    <language>en</language>
    <item>
      <title>Automating Linear From Notion</title>
      <dc:creator>Zachary Sturman</dc:creator>
      <pubDate>Mon, 18 May 2026 22:44:54 +0000</pubDate>
      <link>https://dev.to/zacharysturman/automating-linear-from-notion-2cha</link>
      <guid>https://dev.to/zacharysturman/automating-linear-from-notion-2cha</guid>
      <description>&lt;p&gt;Planning often starts in Notion because it is flexible and easy to shape around a team’s workflow. Execution often moves into Linear because it is structured, fast, and better at handling active delivery. That split works for a while, until the same projects, milestones, and tasks start living in both places and slowly drift out of sync.&lt;/p&gt;

&lt;p&gt;I built a Python service to close that gap. At first, it seemed like a straightforward integration problem: fetch records from Notion, fetch records from Linear, compare them, and push updates where needed.&lt;/p&gt;

&lt;p&gt;It turned out to be more involved than that.&lt;/p&gt;

&lt;p&gt;Once I started dealing with conflicting edits, relationship preservation, duplicate prevention, retry logic, and sync history, the project stopped being a script and became a real synchronization engine. The interesting part was not the API wiring. It was the architecture needed to make the sync reliable enough to trust.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs4608sejsizzmonpd78q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs4608sejsizzmonpd78q.png" alt="hero.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What the Service Does&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;At a high level, the service keeps three kinds of records synchronized between Notion and Linear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Projects&lt;/li&gt;
&lt;li&gt;Milestones&lt;/li&gt;
&lt;li&gt;Tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sync is bidirectional, so changes can originate in either system.&lt;/p&gt;

&lt;p&gt;On each run, the service:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authenticates with both APIs&lt;/li&gt;
&lt;li&gt;Fetches records from Notion and Linear&lt;/li&gt;
&lt;li&gt;Normalizes them into shared internal models&lt;/li&gt;
&lt;li&gt;Compares those models to detect meaningful differences&lt;/li&gt;
&lt;li&gt;Decides whether to create, update, delete, or flag a conflict&lt;/li&gt;
&lt;li&gt;Writes the result back to the opposite system&lt;/li&gt;
&lt;li&gt;Repairs relationships and saves sync state for the next run&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not a one-time import/export tool. It is designed to behave like an ongoing system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FZSturman%2FArticles%2Fmain%2Fautomating-linear-from-notion%2Fimages%2Fnotion_linear_update.mp4" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FZSturman%2FArticles%2Fmain%2Fautomating-linear-from-notion%2Fimages%2Fnotion_linear_update.mp4" alt="notion_linear_update.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why the Problem Is Harder Than It Looks&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The surface mapping seems simple.&lt;/p&gt;

&lt;p&gt;A Notion project page roughly maps to a Linear project.&lt;/p&gt;

&lt;p&gt;A Notion milestone page roughly maps to a Linear milestone.&lt;/p&gt;

&lt;p&gt;A Notion task page roughly maps to a Linear issue.&lt;/p&gt;

&lt;p&gt;But once those entities need to move back and forth reliably, the mismatch appears quickly.&lt;/p&gt;

&lt;p&gt;Notion and Linear do not share the same schema. Their IDs are different. Their status systems are different. Parent-child relationships need to survive sync. Both sides can change the same record before the next run. APIs can fail or rate limit. And if the service loses track of what it already created, duplicates show up fast.&lt;/p&gt;

&lt;p&gt;Even something as small as task status is not a simple field copy.&lt;/p&gt;

&lt;p&gt;In Notion, a task might have a human-readable status like &lt;strong&gt;To Do&lt;/strong&gt; or &lt;strong&gt;In Progress&lt;/strong&gt;. In Linear, an issue belongs to a workflow state with both a label and a state type such as backlog, unstarted, started, or completed.&lt;/p&gt;

&lt;p&gt;A reliable sync cannot compare labels alone. It has to compare meaning.&lt;/p&gt;

&lt;p&gt;That shifts the real problem from “moving data between APIs” to “building a system that can preserve identity, structure, and semantics across two different tools.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ngty9ad6n678ubd329g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ngty9ad6n678ubd329g.png" alt="linear vs notion status.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Design Decision That Made the Project Work&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The design decision that made the whole service workable was introducing a shared internal model layer.&lt;/p&gt;

&lt;p&gt;Instead of comparing raw Notion responses against raw Linear GraphQL nodes, the service translates both into unified Python models first. Projects become UnifiedProject, milestones become UnifiedMilestone, and tasks become UnifiedTask.&lt;/p&gt;

&lt;p&gt;That gives the sync engine a stable internal language.&lt;/p&gt;

&lt;p&gt;Once everything is normalized into a shared representation, the rest of the architecture becomes much cleaner:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The reconciler compares like with like&lt;/li&gt;
&lt;li&gt;Conflict logic operates on consistent fields&lt;/li&gt;
&lt;li&gt;Platform-specific translation stays at the edges&lt;/li&gt;
&lt;li&gt;The codebase no longer scatters Notion-vs-Linear conditionals everywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Notion API data + Linear API data
                ↓
        Unified internal models
                ↓
         Reconcile differences
                ↓
      Write changes to target side
                ↓
      Resolve relationships + save state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unified model is not just a convenience layer. It is what makes reconciliation possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;System Structure&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once the internal model layer was in place, the rest of the service settled into a fairly clean architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Handles environment variables, API credentials, database IDs, field mappings, and conflict strategy settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API clients&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wrap Notion and Linear directly. They handle authentication, requests, pagination, rate limiting, and platform-specific response structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unified models&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Represent the internal source of truth used by the sync engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mappers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Convert records into and out of the unified models. They also translate semantics, such as mapping Notion priority labels to Linear priority values or converting Linear workflow states into a Notion-friendly status representation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reconciler&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compares normalized records and decides whether each one should be created, updated, deleted, or treated as a conflict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relation resolver&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Repairs relationships after base records already exist in both systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State store&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keeps durable sync memory: linked IDs, timestamps, content hashes, and prior sync metadata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Utilities&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cover retries, rate limiting, logging, and CLI support.&lt;/p&gt;

&lt;p&gt;A simplified view of the architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;CLI
→ Config
→ API Clients
→ Unified Models
→ Mappers
→ Reconciler
→ Relation Resolver
→ State Store
→ Notion / Linear APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Following One Task Through the Pipeline&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The easiest way to explain the sync flow is to follow a single task through it.&lt;/p&gt;

&lt;p&gt;Imagine someone creates a task in Notion called &lt;strong&gt;Write API docs&lt;/strong&gt;. They set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;status = &lt;strong&gt;In Progress&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;priority = &lt;strong&gt;High&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;sync enabled = true&lt;/li&gt;
&lt;li&gt;linked project and milestone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The service fetches that page from Notion and parses it into a unified task model. At that point, it is no longer reasoning about raw Notion properties. It is working with an internal task record that contains fields like title, status, priority, relation IDs, and last modified time.&lt;/p&gt;

&lt;p&gt;From there, the mapper translates platform-specific meaning.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notion High priority becomes the Linear priority value expected by the target API&lt;/li&gt;
&lt;li&gt;Notion In Progress becomes a normalized internal state&lt;/li&gt;
&lt;li&gt;That state is then matched to the appropriate Linear workflow meaning rather than copied literally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reconciler compares that unified task against the matching Linear issue, along with persisted sync state from earlier runs.&lt;/p&gt;

&lt;p&gt;If no matching Linear issue exists, the engine creates one.&lt;/p&gt;

&lt;p&gt;If one exists but differs, it updates it.&lt;/p&gt;

&lt;p&gt;If both sides changed incompatibly since the last successful sync, it becomes a conflict.&lt;/p&gt;

&lt;p&gt;Once the write succeeds, the service stores the Notion ID ↔ Linear ID mapping locally so future runs know both records represent the same task.&lt;/p&gt;

&lt;p&gt;If some relationships could not be applied immediately, the relation resolver reconnects them later once all required cross-system IDs exist.&lt;/p&gt;

&lt;p&gt;That miniature pipeline captures the whole system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;normalize → compare → decide → write → reconnect → remember
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;What Counts as a Real Change&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One of the subtler design problems was deciding what should count as a meaningful change.&lt;/p&gt;

&lt;p&gt;A sync engine cannot just ask whether two payloads look different. It has to ask whether they are semantically different in a way that should trigger an update.&lt;/p&gt;

&lt;p&gt;To do that, the service combines several signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;linked identity&lt;/li&gt;
&lt;li&gt;modification timestamps&lt;/li&gt;
&lt;li&gt;normalized content&lt;/li&gt;
&lt;li&gt;persisted sync state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each synced entity, the local store keeps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notion ID&lt;/li&gt;
&lt;li&gt;Linear ID&lt;/li&gt;
&lt;li&gt;last modified times from both sides&lt;/li&gt;
&lt;li&gt;normalized content hash&lt;/li&gt;
&lt;li&gt;last sync metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each signal matters, but none is enough on its own.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IDs tell the system what matches, but not what changed&lt;/li&gt;
&lt;li&gt;Timestamps can be noisy&lt;/li&gt;
&lt;li&gt;Raw payloads can differ in representation without differing in meaning&lt;/li&gt;
&lt;li&gt;State is what makes those signals useful together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where the service stopped behaving like a script and started behaving like an engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Two Things That Made It Reliable&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Two parts of the design mattered more than almost anything else:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;relationship handling&lt;/li&gt;
&lt;li&gt;persistent sync state&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Relationship handling&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Relationships are easy to underestimate.&lt;/p&gt;

&lt;p&gt;A task can belong to a milestone.&lt;/p&gt;

&lt;p&gt;A milestone can belong to a project.&lt;/p&gt;

&lt;p&gt;A task can also have a parent task.&lt;/p&gt;

&lt;p&gt;If every record is synced independently in one pass, many of those references fail simply because the destination record does not exist yet.&lt;/p&gt;

&lt;p&gt;The solution was a two-pass strategy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First pass:&lt;/strong&gt; create or update base records&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second pass:&lt;/strong&gt; repair project, milestone, task, and subtask relationships once cross-system IDs are known&lt;/p&gt;

&lt;p&gt;That second pass is handled by a dedicated relation resolver that uses lookup maps in both directions.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Persistent sync state&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Without local state, the service has no durable memory.&lt;/p&gt;

&lt;p&gt;It can fetch data and compare timestamps, but it cannot truly know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what it created before&lt;/li&gt;
&lt;li&gt;what changed semantically&lt;/li&gt;
&lt;li&gt;whether a record is genuinely new&lt;/li&gt;
&lt;li&gt;whether two records are already linked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the service keeps a local SQLite store with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;linked IDs&lt;/li&gt;
&lt;li&gt;timestamps&lt;/li&gt;
&lt;li&gt;content hashes&lt;/li&gt;
&lt;li&gt;prior sync metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives the engine continuity across runs. It can avoid duplicates, detect changes more accurately, and treat sync as an ongoing process instead of starting from zero each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;One Failure Mode That Changed the Design&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One of the most useful failure cases was also one of the simplest: duplicate creation after partial success.&lt;/p&gt;

&lt;p&gt;Imagine the service successfully creates a record in the target system, but fails before persisting the new linkage locally.&lt;/p&gt;

&lt;p&gt;On the next run, that same source record can still look unsynced. The engine may then create a second copy.&lt;/p&gt;

&lt;p&gt;That is the kind of bug that does not show up in a clean demo but appears quickly in a real system.&lt;/p&gt;

&lt;p&gt;It reinforced an important design lesson: local sync state is not just metadata for convenience. It is part of the identity model of the system.&lt;/p&gt;

&lt;p&gt;The same pattern showed up with relationships. Creating a task before its milestone mapping existed was not really an API bug. It was a sequencing problem.&lt;/p&gt;

&lt;p&gt;That realization pushed the design toward explicit relation repair instead of trying to force everything into a single perfect pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Making It Safe to Run&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once the happy path worked, the next question was whether the system behaved well under ordinary failure conditions.&lt;/p&gt;

&lt;p&gt;That meant adding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry logic with exponential backoff&lt;/li&gt;
&lt;li&gt;explicit rate limit handling&lt;/li&gt;
&lt;li&gt;dry runs through the CLI&lt;/li&gt;
&lt;li&gt;structured logging around every sync action&lt;/li&gt;
&lt;li&gt;configurable conflict resolution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The retry layer wraps API calls with backoff and jitter, while rate limiters help avoid throttling before it happens.&lt;/p&gt;

&lt;p&gt;The CLI also matters more than it might seem. The sync can run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bidirectionally&lt;/li&gt;
&lt;li&gt;Notion → Linear only&lt;/li&gt;
&lt;li&gt;Linear → Notion only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It can also be scoped to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;projects only&lt;/li&gt;
&lt;li&gt;milestones only&lt;/li&gt;
&lt;li&gt;tasks only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And dry-run mode lets you inspect intended actions before allowing writes.&lt;/p&gt;

&lt;p&gt;Conflict handling had to be configurable too. In a bidirectional sync, there is no universal answer to which side should win when both records changed.&lt;/p&gt;

&lt;p&gt;Depending on the workflow, the right strategy might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;last-write-wins&lt;/li&gt;
&lt;li&gt;Notion-primary&lt;/li&gt;
&lt;li&gt;Linear-primary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those operational features are less visible than the core data flow, but they are what make the tool usable outside of controlled demos.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What I Learned&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from the project is that synchronization problems stop being API problems almost immediately.&lt;/p&gt;

&lt;p&gt;At first, I assumed most of the work would be authentication, field mapping, and request handling. Those pieces mattered, but they were not the center of the challenge.&lt;/p&gt;

&lt;p&gt;The hard part was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;defining a stable internal model&lt;/li&gt;
&lt;li&gt;preserving relationships across systems&lt;/li&gt;
&lt;li&gt;deciding what changed in a meaningful way&lt;/li&gt;
&lt;li&gt;keeping enough memory of earlier runs to avoid guessing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, the core problem was not moving data. It was establishing internal truth.&lt;/p&gt;

&lt;p&gt;Once that became clear, the APIs stopped being the main event. They became edges. The architecture in the middle became the actual product.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Tradeoffs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;That architecture comes with tradeoffs.&lt;/p&gt;

&lt;p&gt;A unified internal model makes reconciliation much easier, but it also means the mapping layer has to evolve as either API changes.&lt;/p&gt;

&lt;p&gt;Persistent local state makes the system safer and more reliable, but it adds operational surface area. Once identity, timestamps, and hashes are stored locally, that state becomes part of the product.&lt;/p&gt;

&lt;p&gt;And bidirectional sync is far more useful than a one-way export, but it guarantees that conflict resolution is not an edge case. It is part of normal operation.&lt;/p&gt;

&lt;p&gt;Those tradeoffs were worth it. But reliability here comes from accepting complexity and placing it deliberately at the center of the system rather than pretending it is not there.&lt;/p&gt;

&lt;p&gt;If you want the broader workflow around that sync, I wrote more about how I use Notion to track my projects at &lt;a href="https://zachary-sturman.com/articles/how-i-use-notion-to-track-my-projects" rel="noopener noreferrer"&gt;https://zachary-sturman.com/articles/how-i-use-notion-to-track-my-projects&lt;/a&gt; and how I sync my portfolio using Notion at &lt;a href="https://zachary-sturman.com/articles/how-i-sync-my-portfolio-using-notion" rel="noopener noreferrer"&gt;https://zachary-sturman.com/articles/how-i-sync-my-portfolio-using-notion&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If this kind of system design and reliability-focused architecture is interesting to you, you can explore more of my work at &lt;a href="https://zachary-sturman.com" rel="noopener noreferrer"&gt;zachary-sturman.com&lt;/a&gt;, or dive into the full implementation on GitHub: &lt;a href="https://github.com/ZSturman/Linear-Notion-Sync" rel="noopener noreferrer"&gt;https://github.com/ZSturman/Linear-Notion-Sync&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>apiintegration</category>
      <category>automation</category>
      <category>linear</category>
      <category>notion</category>
    </item>
    <item>
      <title>How I Sync My Portfolio Using Notion</title>
      <dc:creator>Zachary Sturman</dc:creator>
      <pubDate>Tue, 12 May 2026 11:59:51 +0000</pubDate>
      <link>https://dev.to/zacharysturman/how-i-sync-my-portfolio-using-notion-3po0</link>
      <guid>https://dev.to/zacharysturman/how-i-sync-my-portfolio-using-notion-3po0</guid>
      <description>&lt;p&gt;For a while now, I have wanted my portfolio to work less like a hand-edited website and more like a publishing pipeline.&lt;/p&gt;

&lt;p&gt;I do not want to update the same project information in five places. I do not want portfolio pages to be their own isolated source of truth. I want project data to live upstream in a structured system, get transformed once, and then flow into the site in a predictable way.&lt;/p&gt;

&lt;p&gt;That is the setup I use now.&lt;/p&gt;

&lt;p&gt;At a high level, the process is simple: I manage project information in Notion, use n8n to assemble that data into a JSON export, run local Python build scripts to turn that export into a static content layer, optimize the media, sync in articles from a separate source, then let Next.js export the site and Firebase serve it.&lt;/p&gt;

&lt;p&gt;A lot of the structure in this pipeline is also a holdover from an older project of mine called Folio. The current site no longer runs through Folio itself, but parts of the naming and architecture still come from that earlier phase. That is why one of the main scripts is still called lib/folio-prebuild.py even though the portfolio is now built from Notion, n8n, JSON exports, and static publishing.&lt;/p&gt;

&lt;p&gt;I am writing more about that backstory separately at &lt;a href="https://zachary-sturman.com/articles/the-art-of-turning-a-90-minute-task-into-a-2-month" rel="noopener noreferrer"&gt;zachary-sturman.com/articles/the-art-of-turning-a-90-minute-task-into-a-2-month&lt;/a&gt;, but the short version is that I spent a long time trying to build a structured project record that could feed multiple outputs. I no longer use that system directly, but the output-oriented thinking survived, and this pipeline is what that evolved into.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Notion is where the content starts&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpo44731swtil4amvyv6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpo44731swtil4amvyv6.png" alt="notion projhects db.png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The source of truth for this setup starts in Notion.&lt;/p&gt;

&lt;p&gt;That is where I keep the project information that eventually becomes the portfolio: titles, summaries, statuses, assets, resources, collections, work logs, and other structured relationships that help define a project beyond just a name and thumbnail.&lt;/p&gt;

&lt;p&gt;I have a separate article about how I use Notion for this in more detail here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zachary-sturman.com/articles/how-i-use-notion-to-track-my-projects" rel="noopener noreferrer"&gt;https://zachary-sturman.com/articles/how-i-use-notion-to-track-my-projects&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I do not want to repeat all of that here. The relevant part for this article is just the handoff: Notion is where the information is authored and organized, but it is not where the portfolio gets assembled.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;The website does not query Notion directly. The frontend does not know how my databases are structured. The build process does not depend on live requests into my workspace. Instead, I use Notion as the editorial layer, then convert that into a local export the site can build from.&lt;/p&gt;

&lt;p&gt;That keeps the website side simpler and makes the publishing process more deterministic.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The shape of this pipeline still comes from Folio&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One thing that is probably worth explaining early is why parts of this setup still look the way they do.&lt;/p&gt;

&lt;p&gt;The current portfolio pipeline grew out of an older system I built called Folio. That older work went through a lot of versions, but one of the most consistent ideas across them was this: a project should exist as a structured record that can feed multiple outputs.&lt;/p&gt;

&lt;p&gt;That could mean a portfolio page, a local document, a media view, an archive, or something else. The exact interface changed a lot over time, but the idea of one structured project record powering more than one destination kept surviving.&lt;/p&gt;

&lt;p&gt;That is the relevant part here.&lt;/p&gt;

&lt;p&gt;The current pipeline does not use Folio as its runtime system, but it still carries some of that architecture forward. The naming is one example. The reason the build script is still called folio-prebuild.py is not because the current site depends on Folio. It is because the pipeline was reworked from that earlier structure instead of being renamed from scratch after every architectural shift.&lt;/p&gt;

&lt;p&gt;So if some of the codebase has older names attached to newer responsibilities, that is why.&lt;/p&gt;

&lt;p&gt;If you do not care about the longer backstory, the short version is this: I used to try solving this problem with a much more custom system. Now I use Notion for the editing layer, but I kept the idea that structured project data should be transformed into a reusable content layer before the frontend touches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;n8n turns the Notion data into a build input&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwy4158gxfvmkhmind0r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwy4158gxfvmkhmind0r.png" alt="n8n workflow.png" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next step is n8n.&lt;/p&gt;

&lt;p&gt;This is the bridge between the editorial structure in Notion and the actual build input used by the portfolio.&lt;/p&gt;

&lt;p&gt;My n8n workflow pulls data from several parts of my Notion setup, merges the records together, reshapes them into the structure I want, and writes out a JSON file that the local build step can consume.&lt;/p&gt;

&lt;p&gt;In the screenshot above, the workflow is doing a few distinct jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;it starts from a trigger&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it queries the relevant Notion databases&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it merges those streams together&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it assembles the nested JSON structure&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it writes the export to disk&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it logs success or failure back into Notion&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That export becomes the portfolio handoff.&lt;/p&gt;

&lt;p&gt;This is a useful separation point in the system because it means the site build does not need to know anything about Notion’s API, my database layout, or the internal structure of my workspace. n8n handles the extraction and reshaping, and the repo only needs to deal with the exported file.&lt;/p&gt;

&lt;p&gt;That file is new_projects.json.&lt;/p&gt;

&lt;p&gt;Once that exists, the website side can treat it as the source input and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the routes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqffr79x5le70y7ooi4q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqffr79x5le70y7ooi4q.png" alt="terminal code composite.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the export exists, the portfolio build becomes a local file transformation problem.&lt;/p&gt;

&lt;p&gt;That is the point where I stop thinking in terms of Notion pages and start thinking in terms of static site inputs.&lt;/p&gt;

&lt;p&gt;The main entry point for that part of the process is lib/folio-prebuild.py.&lt;/p&gt;

&lt;p&gt;Even the top docstring in that file says exactly what it is doing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build public/projects from n8n-exported new_projects.json.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the real handoff.&lt;/p&gt;

&lt;p&gt;The script takes the JSON export, passes it into the normalization pipeline, builds the public project output, writes supporting manifests, and publishes the result into the public directory where the site can use it.&lt;/p&gt;

&lt;p&gt;The command that matters most here is basically this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;
npm run generate-projects

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

&lt;/div&gt;



&lt;p&gt;And under the hood, that maps to a Python build step that points at the exported JSON file and runs lib/folio-prebuild.py.&lt;/p&gt;

&lt;p&gt;I like this setup because it makes the expensive content-building work explicit. It is not hidden inside deployment. It is not mixed into the frontend runtime. It is a clear, local step.&lt;/p&gt;

&lt;p&gt;That also makes it easier to reason about when something goes wrong. If there is a bad field, a missing asset, a malformed relationship, or a path issue, I can catch it at the build layer before it becomes a broken route on the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The project build step turns raw records into website-shaped data&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftoe5e6l2jaej7o7i0wq5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftoe5e6l2jaej7o7i0wq5.png" alt="portfolio built.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the core of the whole process.&lt;/p&gt;

&lt;p&gt;The job of the project build step is not just to copy a JSON file from one place to another. It is to take data that is still shaped like an export and normalize it into something the site can actually trust.&lt;/p&gt;

&lt;p&gt;That includes a few important steps.&lt;/p&gt;

&lt;p&gt;First, it validates the input and builds a normalized project representation. The entry script delegates most of that work into projects_pipeline.py, but folio-prebuild.py makes the role clear: it creates a temporary build directory, calls the pipeline, writes the final output, and atomically replaces the public projects folder.&lt;/p&gt;

&lt;p&gt;Second, it creates the project manifest the frontend uses. The script writes a projects.json file into the generated output. That becomes the main data layer for project content on the frontend side.&lt;/p&gt;

&lt;p&gt;Third, it writes image-hostnames.json, which gives the frontend a controlled list of external image hosts that are allowed. That is a small detail, but it is part of what makes the build output feel like a complete content layer instead of just a loose export dump.&lt;/p&gt;

&lt;p&gt;Fourth, it handles a lot of defensive filesystem behavior. This is one of the places where the older pipeline DNA is still visible, and I think it is useful. The build step uses a lockfile so I do not accidentally run overlapping builds. It writes into a temporary directory first. It atomically replaces the public output when the build succeeds. It keeps a backup path if the old directory cannot be removed cleanly. It also includes repair logic for the cloud-sync issue where a folder can get renamed to something like projects 2 instead of staying canonical.&lt;/p&gt;

&lt;p&gt;That part is not glamorous, but it is the kind of detail that makes a local publishing pipeline more trustworthy.&lt;/p&gt;

&lt;p&gt;This is also the stage where one abstract project record becomes something much closer to a real page on the site. Titles, slugs, paths, media locations, related resources, collections, work logs, and article references all start getting shaped into the form the frontend expects.&lt;/p&gt;

&lt;p&gt;This is where the portfolio stops being “my project data” and starts being “the site’s content layer.”&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Every project gets a canonical route and a stable public folder&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6m4ykd4547e7nikj9stp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6m4ykd4547e7nikj9stp.png" alt="browser - topnote.png" width="800" height="981"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of the important outcomes of the build step is that each project gets a canonical route and a stable public folder.&lt;/p&gt;

&lt;p&gt;That matters because a portfolio stops feeling solid pretty quickly if the URL structure is inconsistent or if media paths are fragile.&lt;/p&gt;

&lt;p&gt;The pipeline solves that by normalizing titles into stable slugs, generating clean hrefs, and copying referenced media into project-specific folders under public/projects/....&lt;/p&gt;

&lt;p&gt;In practice that means the project data can be authored upstream however I need it to be, but by the time it reaches the site it has a canonical route shape. The frontend is not guessing how to build project URLs from raw records. It gets a clean manifest that already encodes that decision.&lt;/p&gt;

&lt;p&gt;Conceptually it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;
project record

→ normalized slug

→ canonical href

→ generated project folder in public/projects/

→ route rendered by the site

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

&lt;/div&gt;



&lt;p&gt;That is one of the reasons I like doing the normalization before the frontend touches anything. It keeps the React and Next.js side of the system much thinner.&lt;/p&gt;

&lt;p&gt;The frontend does not need to negotiate the messy version of the data. It only has to render the cleaned version.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Articles are a second content source&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Futtru5f5o1lxd9c7i240.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Futtru5f5o1lxd9c7i240.png" alt="browser - articles.png" width="800" height="981"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The portfolio is not built from project JSON alone.&lt;/p&gt;

&lt;p&gt;Articles come in through a separate sync process.&lt;/p&gt;

&lt;p&gt;That matters because the site is not just a projects grid. It also includes writing, and I wanted articles to live in a structure that could be generated and normalized the same way instead of being hand-wired page by page.&lt;/p&gt;

&lt;p&gt;The article sync stage pulls from a separate repository, discovers the available markdown content, rewrites relative links into portfolio-local paths, copies referenced assets, and builds out a normalized article structure under public/articles.&lt;/p&gt;

&lt;p&gt;It also resolves project references in article frontmatter into canonical project IDs. That is a small but important detail, because it keeps article-to-project links stable even if the original reference came in as a title, slug, or some other upstream identifier.&lt;/p&gt;

&lt;p&gt;That gives me a content model where projects and articles are separate sources, but both end up flowing into the same static publishing layer.&lt;/p&gt;

&lt;p&gt;So even though the project data and article content start in different places, they get normalized into a shared output shape before the site is exported.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Media optimization is part of publishing, not just cleanup&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After the raw project media is copied into the public project folders, there is another step that matters just as much: optimization.&lt;/p&gt;

&lt;p&gt;That happens in lib/media-optimizer.py.&lt;/p&gt;

&lt;p&gt;The goal here is to generate the versions of the media that the site actually wants to serve, not just preserve the originals.&lt;/p&gt;

&lt;p&gt;For images, that means creating optimized WebP versions, smaller thumbnail variants, and blur placeholders for loading states. For videos, it means generating smaller web-ready MP4 files along with thumbnails and placeholders. For some 3D assets, it can also convert models into formats that are easier to serve on the web.&lt;/p&gt;

&lt;p&gt;The point is not just to compress files for the sake of compression. It is to make the output layer actually reflect the way the frontend wants to consume media.&lt;/p&gt;

&lt;p&gt;That is why I think of this as part of publishing rather than maintenance.&lt;/p&gt;

&lt;p&gt;The script itself is very direct about this. It defines the optimized variants, checks for tools like ffmpeg, handles images, videos, SVGs, and some 3D models, and writes the generated assets alongside the originals with consistent suffixes like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;
image.jpg

image-optimized.webp

image-thumb.webp

image-placeholder.jpg

video.mp4

video-optimized.mp4

video-thumb.jpg

video-placeholder.jpg

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

&lt;/div&gt;



&lt;p&gt;That is also why the Git strategy makes sense the way it does. The optimized derivatives are part of the site output. They are not disposable side products.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The frontend treats the generated output like a static content API&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6kmzjupfd8szmod2nqs1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6kmzjupfd8szmod2nqs1.png" alt="browser - home page.png" width="800" height="971"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the project build, article sync, and media optimization steps are finished, Next.js can treat the generated files as a content layer.&lt;/p&gt;

&lt;p&gt;That is one of the cleanest parts of the architecture.&lt;/p&gt;

&lt;p&gt;The frontend does not need to know where the data came from upstream. It does not need to understand Notion or n8n. It does not need to rebuild relationships on the fly. It just reads the manifests and files that were already generated.&lt;/p&gt;

&lt;p&gt;The homepage reads from the generated project manifest. Project detail pages and article routes are derived from the generated content. Media helpers can request the optimized variants automatically. And because the site is configured for static export, the build writes a fully static output that can be hosted directly.&lt;/p&gt;

&lt;p&gt;That means the architecture ends up looking something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;
Notion + article source

→ export + sync

→ normalized manifests + copied media

→ optimized delivery assets

→ Next.js static export

→ deployed portfolio

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

&lt;/div&gt;



&lt;p&gt;I like this because it gives me a dynamic editing process upstream and a very static, predictable delivery layer downstream.&lt;/p&gt;

&lt;p&gt;Those are different jobs, and I do not think they need to be solved in the same place.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;I keep the expensive assembly work local before push&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0a8hcycgctbf3jg8uddi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0a8hcycgctbf3jg8uddi.png" alt="CICD.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of the biggest design decisions in this setup is that content generation happens before push, not inside CI.&lt;/p&gt;

&lt;p&gt;That means the local machine does the heavier work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;generate project data from the exported JSON&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;sync articles&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;optimize media&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;verify the output&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then CI stays relatively thin.&lt;/p&gt;

&lt;p&gt;GitHub Actions does not regenerate project data, rebuild article content from scratch, or run the expensive media pipeline as the primary publishing step. It mainly installs dependencies, runs the site build, and deploys the already-generated state.&lt;/p&gt;

&lt;p&gt;I like that separation for a few reasons.&lt;/p&gt;

&lt;p&gt;First, it keeps deploys faster and more deterministic. The build server is not trying to replicate my whole content-generation environment.&lt;/p&gt;

&lt;p&gt;Second, it means the checked-in generated artifacts are part of the known state of the repo. The build is using a precomputed content layer instead of hoping the upstream dependencies behave the same way in CI every time.&lt;/p&gt;

&lt;p&gt;Third, it makes the workflow easier to reason about. The assembly process is local and inspectable. The deploy process is mostly just packaging and publishing.&lt;/p&gt;

&lt;p&gt;That is a much calmer model than doing everything in one step on every push.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;There are a lot of guardrails because local build pipelines are easy to trust until they break&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgjca6yopwo9lknsivl3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgjca6yopwo9lknsivl3.png" alt="git push tests.png" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A local publishing setup like this is only useful if it is hard to accidentally break.&lt;/p&gt;

&lt;p&gt;That is where a lot of the more defensive details start to matter.&lt;/p&gt;

&lt;p&gt;The build script uses a lockfile so I do not run overlapping builds into the same public directory. It writes into a temporary directory first and only replaces the output when the build has succeeded. It can back up the old generated folder before replacement. It includes cleanup logic for stale backups and stray sibling directories. And it even has post-build verification and repair logic for cloud-sync rename collisions.&lt;/p&gt;

&lt;p&gt;The media layer has similar practical concerns. It checks for tool availability, handles different file types differently, and can batch-optimize entire directories while skipping already-generated derivatives.&lt;/p&gt;

&lt;p&gt;None of that is particularly article-friendly in a visual sense, but it is the part of the system that keeps the whole thing from feeling brittle.&lt;/p&gt;

&lt;p&gt;There is also a broader quality gate before deployment. Unit tests cover transformation logic, browser tests cover key user-facing flows, and the pre-push hook is strict about generated media being in a clean state.&lt;/p&gt;

&lt;p&gt;That is important to me because generated content pipelines are one of those things that can feel stable right up until one missing asset, stale file, or half-finished build quietly makes it into production.&lt;/p&gt;

&lt;p&gt;I would rather the system be annoying early than surprising late.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why I still like this architecture&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The main reason I like this setup is that it gives me a good boundary between authoring and publishing.&lt;/p&gt;

&lt;p&gt;Notion is good for structured editing and maintaining the project record. n8n is good at assembling that into a clean handoff. Python is good at normalization, copying, repairing, and turning that handoff into a static content layer. Next.js is good at rendering the final result once the content is already in the right shape.&lt;/p&gt;

&lt;p&gt;Each part has a clear job.&lt;/p&gt;

&lt;p&gt;And even though a lot of this architecture still carries the shape of older experiments like Folio, I think that is fine. That history is the reason the system looks the way it does. I did not arrive at this setup by designing a perfectly clean pipeline from scratch. I arrived here by repeatedly trying to solve the same underlying problem, then keeping the parts that still felt useful once I stopped wanting to maintain a fully custom app.&lt;/p&gt;

&lt;p&gt;So the current portfolio is less of a standalone site and more of a publishing endpoint.&lt;/p&gt;

&lt;p&gt;The work of structuring the project data happens upstream. The work of normalizing and packaging it happens locally. The site just serves the result.&lt;/p&gt;

&lt;p&gt;That feels like the right division for me right now.&lt;/p&gt;

&lt;p&gt;If you are building something similar, that is probably the main takeaway I would offer: do not make your frontend solve editorial and publishing problems if you can solve them once upstream instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Closing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This pipeline is not just a way of updating my website. It is the current version of a longer idea I have been iterating on for years.&lt;/p&gt;

&lt;p&gt;The older Folio work is the reason parts of the naming and structure still look the way they do. Notion is the reason the day-to-day editing side now feels lighter. n8n is the bridge that turns structured records into a usable export. The Python scripts are where the portfolio becomes website-shaped. And the static build is what lets the final site stay simple.&lt;/p&gt;

&lt;p&gt;That combination works better for me than trying to make one custom tool do everything.&lt;/p&gt;

&lt;p&gt;It lets me keep the structured project record idea that I still care about, without also turning the maintenance of the system itself into the main project.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cicd</category>
      <category>firebase</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Building a Location-First Learning Agent to Explore Context, Memory, and Consciousness</title>
      <dc:creator>Zachary Sturman</dc:creator>
      <pubDate>Sat, 02 May 2026 22:56:38 +0000</pubDate>
      <link>https://dev.to/zacharysturman/building-a-location-first-learning-agent-to-explore-context-memory-and-consciousness-3lnk</link>
      <guid>https://dev.to/zacharysturman/building-a-location-first-learning-agent-to-explore-context-memory-and-consciousness-3lnk</guid>
      <description>&lt;p&gt;Maybe recognition is less about detached labels than it is about where something is, what surrounds it, and how that context gets reinforced. That is what pushed me to build this repo. I wanted a smaller problem that still felt close to the thing I cared about. The question that kept pulling me back was whether location and context are a big part of knowing something at all, and whether they help make object recognition faster and more grounded.&lt;/p&gt;

&lt;p&gt;That led me to build a small location-first learning agent. Right now it is a Python CLI project that learns from scalar observations and simple file-backed sensor inputs, stores what it learns in plain JSON and JSONL, and keeps the whole state inspectable. It is not a finished cognitive architecture, and it does not pretend to be one. What it gives me instead is a controlled place to test a very specific idea: maybe recognition does not start with detached labels, maybe it starts with where something is, what surrounds it, and how that context gets reinforced over time.&lt;/p&gt;

&lt;p&gt;I also made a deliberate structural choice early on. I started out working with Thousand Brains Project Monty, but I split this work into a separate repo because I wanted a simpler environment where I could move directly on the parts I was most interested in. I did not want to spend my time threading a new experiment through a larger existing system before I understood the experiment itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffuomm6i11xxtoyz4s6c3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffuomm6i11xxtoyz4s6c3.png" alt="1. Phase roadmap.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I started with location
&lt;/h2&gt;

&lt;p&gt;There are plenty of ways to make a toy agent look smarter than it is. I was more interested in making one that was inspectable. That meant I needed a narrow problem, a tight loop, and data structures I could read without translation.&lt;/p&gt;

&lt;p&gt;So the first version of this project was almost stubbornly small. It learned grayscale observations mapped to location labels, persisted them across sessions, and logged each interaction. That sounds minimal, and it is, but it gave me a starting point for a question I still care about: if an agent repeatedly encounters a signal in a place-like context, what should it actually remember, and what should count as the identity of that memory?&lt;/p&gt;

&lt;p&gt;The repo has moved through a few clear phases since then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Phase 1 bootstrapped exact-match grayscale memory with persistence and append-only logging.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Phase 2 added noisy scalar matching and confidence thresholds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Phase 3 merged repeated observations into location models instead of treating every value as a separate record.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Phase 4, the current phase, introduces first-class labels, aliases, rename history, sensor bindings, and provenance-aware evidence records.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That progression matters to me because it shows the shape of the project. I am not trying to jump straight from nothing to a full theory of mind. I am trying to build up a memory system that stays small enough to reason about while it gets more structured.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3253072bg9itputsv99c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3253072bg9itputsv99c.png" alt="6 schema plan.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the current project actually does
&lt;/h2&gt;

&lt;p&gt;At the moment, this project is a stdlib-only Python CLI with no external packages required. I can run it, enter a grayscale value between &lt;code&gt;0.0&lt;/code&gt; and &lt;code&gt;1.0&lt;/code&gt;, and either teach it a new location label or confirm a guessed one. It stores learned state in &lt;code&gt;runtime/location_memory.json&lt;/code&gt; and appends interaction events to &lt;code&gt;runtime/agent_events.jsonl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That by itself would be a modest memory toy, but the current phase adds a few pieces that made the project feel more meaningful to me.&lt;/p&gt;

&lt;p&gt;First, the system no longer treats the label as the identity. A location model now points to a &lt;code&gt;label_id&lt;/code&gt;, and the label itself lives as a separate node with a canonical name, aliases, and rename history. That sounds like a small refactor, but it changed the shape of the project. Once I separated the thing being remembered from the latest name attached to it, a lot of awkward cases stopped feeling awkward. A label could change without pretending the location itself had changed. An alias could be useful without becoming a duplicate. A wrong guess could be corrected without discarding the old path through the system.&lt;/p&gt;

&lt;p&gt;Second, scalar matching is not just nearest-prototype matching anymore. If the same location is confirmed across a wider range of observations, the system treats that inclusive span as learned territory. In practical terms, if I teach one location at &lt;code&gt;0.10&lt;/code&gt; and &lt;code&gt;0.30&lt;/code&gt;, a later value like &lt;code&gt;0.28&lt;/code&gt; can default to that same place unless conflicting evidence shows up. I like this because it feels closer to how a memory should broaden, not just average.&lt;/p&gt;

&lt;p&gt;Third, the current phase adds a sensor preview through &lt;code&gt;sense /path/to/file&lt;/code&gt;. This lets the agent learn or recognize a file-backed input and bind it to a location. The important limitation is that it does this through exact file fingerprinting. It hashes the file contents, stores the fingerprint, and re-recognizes that exact input later. That is useful as a stepping stone, but it is not the final perception model I want. I have been careful in the repo docs to describe it as a temporary preview, because I do not want a convenient shortcut to quietly become the architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fql9jb39pr4h6rf8xd2ty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fql9jb39pr4h6rf8xd2ty.png" alt="3. Runtime persistence.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The design decision that mattered most: identity is not the same thing as a name
&lt;/h2&gt;

&lt;p&gt;The strongest technical decision in the current codebase is also the one that feels most conceptually important to me: label identity should be separate from the human-readable name.&lt;/p&gt;

&lt;p&gt;In earlier versions, the mapping between a learned signal and a label was much flatter. That worked for bootstrapping, but it also made the memory feel brittle. If the name changed, the memory structure had to behave as if the thing itself changed. If I wanted synonyms or better naming later, I was really just patching a string field.&lt;/p&gt;

&lt;p&gt;Phase 4 changes that by introducing &lt;code&gt;LabelNode&lt;/code&gt; and storing label ownership through &lt;code&gt;label_id&lt;/code&gt;. The location model keeps its own identity. The label keeps its naming history. Aliases stay attached to the same node. Wrong-guess corrections can rename the canonical label while preserving the old name as an alias. That is a cleaner technical model, but it also gets closer to the question that motivated the project in the first place. A remembered thing should not vanish just because I describe it better later.&lt;/p&gt;

&lt;p&gt;This also made the inspectability better. The &lt;code&gt;inspect&lt;/code&gt; command can now surface canonical label, aliases, label id, prototype, spread, observation count, and rename count in one place. The runtime schema also carries &lt;code&gt;sensor_bindings&lt;/code&gt;, &lt;code&gt;graph_edges&lt;/code&gt;, &lt;code&gt;concept_nodes&lt;/code&gt;, and &lt;code&gt;evidence_records&lt;/code&gt;, even though some of that is still scaffolding for later phases. I like that the state can be read directly. I can see what the agent knows, why it knows it, and which parts are still placeholders for future work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0afncu4cp33sq0m58j9i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0afncu4cp33sq0m58j9i.png" alt="4. end to end sequence.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The second big decision: keep the sensor preview useful, but refuse to confuse it with perception
&lt;/h2&gt;

&lt;p&gt;One of the easier traps in projects like this is letting a convenient shortcut turn into a story about capability. The current image sensing path is useful because it gives me a way to test the learning loop with actual files and a repo-local media pack. The repo now includes generated-local Phase 4 fixtures, a media catalog, and scenario manifests so that part of the project stays deterministic and testable.&lt;/p&gt;

&lt;p&gt;That part is real, and I think it is valuable.&lt;/p&gt;

&lt;p&gt;What is not real yet is content-based perception. The system is not identifying rooms from visual structure, learned features, or scene composition. It is binding exact file fingerprints. The docs are explicit that later phases should move toward an &lt;code&gt;ObservationBundle&lt;/code&gt; contract, region attention, primitive percepts, cue composition, and eventually a broader memory-and-attention engine. I think that distinction matters, because otherwise it would be easy to oversell what is happening now.&lt;/p&gt;

&lt;p&gt;For the article, I want to be clear about both sides of that. The current preview is not nothing. It gave me a practical way to test location binding with deterministic image fixtures. But it also is not the end state, and I do not want to talk about it as if it already solves perception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I kept the implementation plain
&lt;/h2&gt;

&lt;p&gt;This project is intentionally plain in a few ways. It is stdlib-only. The CLI is synchronous and line-oriented. Persistence is single-writer JSON and JSONL. None of that is glamorous, and I am fine with that.&lt;/p&gt;

&lt;p&gt;The benefit is that the moving pieces stay readable. The memory store is a JSON document. The event log is append-only. The tests say what the current phase is expected to do. The decisions file says why I changed the model when I did. Even the limitations are documented pretty directly, including the fact that Phase 4 still has pending manual acceptance even though the automated suite passes.&lt;/p&gt;

&lt;p&gt;That simplicity fits the purpose of the project. I am not trying to hide the structure behind a polished interface yet. I am trying to make the structure legible enough that I can tell when a design choice actually helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is implemented, and what is still only a roadmap
&lt;/h2&gt;

&lt;p&gt;This is where I think a lot of projects get muddy, so I want to keep it clean.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;persistent scalar learning with confidence and span-aware matching&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;first-class labels with aliases and rename history&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;exact-file sensor binding for a temporary image-preview path&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;provenance-aware evidence records restricted to user or sensor sources&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;repo-local fixture images, scenario manifests, and validation checks&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Documented for later, but not implemented yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ObservationBundle&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ExperienceFrame&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;MemoryUnit&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;activation competition&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;replay&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;resurfacing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;reconsolidation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;richer body-relative and multimodal context&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have already written those future concepts into the roadmap because I want a stable direction for the project, but I do not want to collapse the distinction between "planned" and "running." Right now this is still a location-first learning system with a broader cognitive direction, not a finished memory engine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18mno073mdnkq17fwy39.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18mno073mdnkq17fwy39.png" alt="2. Observational Bundle.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned from building it this way
&lt;/h2&gt;

&lt;p&gt;The main thing I learned is that narrowing the scope did not make the project less interesting. It made the interesting parts easier to see.&lt;/p&gt;

&lt;p&gt;Separating label identity from naming made the whole memory story cleaner. Treating repeated confirmations as reinforcement rather than duplication made the learned state feel more coherent. Keeping the sensor preview deliberately temporary forced me to write down what I actually mean by perception instead of hiding behind a convenient shortcut.&lt;/p&gt;

&lt;p&gt;I also learned that inspectability changes how I think about progress. A project like this can sound more advanced than it is if I only describe the aspiration. The files, tests, schema versions, and event logs keep me honest. They give me something concrete to evaluate, and they make it easier to notice when a new feature is really just a patch over a muddled model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I want to take it next
&lt;/h2&gt;

&lt;p&gt;The project is still a work in progress, and I plan to keep iterating and testing it in my free time. The next steps in the repo point toward richer context, typed concept scaffolding, and eventually a more general memory-and-attention layer, but I am still treating the current implementation as the thing that has to earn the next layer.&lt;/p&gt;

&lt;p&gt;That is probably the clearest way I can describe the project right now. I started it because I wanted a personal space to test ideas about consciousness. I narrowed it to location and context because that felt like something I could actually build and inspect. Now I am using that smaller system to figure out which ideas survive contact with code.&lt;/p&gt;

&lt;p&gt;I will post more about it after I have a better sense of what it can and cannot do. If you have thoughts about location, context, or memory design, I would be interested to hear them. The project is still early enough that those conversations can still influence how I shape the next layers.&lt;/p&gt;

&lt;p&gt;If you want the broader architectural direction this is growing into, I wrote more about that here: &lt;a href="https://zachary-sturman.com/articles/consolidating-an-offline-first-episodic-memory-system" rel="noopener noreferrer"&gt;https://zachary-sturman.com/articles/consolidating-an-offline-first-episodic-memory-system&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhnosm0s9jle6o9lr2xki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhnosm0s9jle6o9lr2xki.png" alt="01v2_cli_to_core_execution_map.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to follow along you can find the repo here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Repo:  &lt;a href="https://github.com/ZSturman/Train-of-Thought-Agent" rel="noopener noreferrer"&gt;https://github.com/ZSturman/Train-of-Thought-Agent&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GitHub: &lt;a href="https://github.com/zsturman" rel="noopener noreferrer"&gt;github.com/zsturman&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LinkedIn: &lt;a href="https://linkedin.com/in/zacharysturman" rel="noopener noreferrer"&gt;linkedin.com/in/zacharysturman&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Portfolio: &lt;a href="https://zachary-sturman.com/" rel="noopener noreferrer"&gt;zachary-sturman.com&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Email: &lt;a href="mailto:Zasturman@gmail.com"&gt;Zasturman@gmail.com&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>adaptive</category>
      <category>cli</category>
      <category>learning</category>
    </item>
    <item>
      <title>The Wolf Project - Reworking a Real-World Project</title>
      <dc:creator>Zachary Sturman</dc:creator>
      <pubDate>Mon, 27 Apr 2026 21:06:25 +0000</pubDate>
      <link>https://dev.to/zacharysturman/the-wolf-project-reworking-a-real-world-project-5e3m</link>
      <guid>https://dev.to/zacharysturman/the-wolf-project-reworking-a-real-world-project-5e3m</guid>
      <description>&lt;p&gt;A few weeks ago I watched a Dodo video about The Wolf Project that stayed with me more than I expected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=NMf8mlxO4sk" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=NMf8mlxO4sk&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, I tried to find more information about what I had just watched. When I googled ‘the wolf project’ there was obviously a lot of options so I went to their Instagram page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.instagram.com/_wolf_project/" rel="noopener noreferrer"&gt;https://www.instagram.com/_wolf_project/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I sent a message, Megan responded, and she made it clear that help on the site itself would actually be useful. That became the starting point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the Project Started
&lt;/h2&gt;

&lt;p&gt;The existing site did what it needed to do at a basic level. It explained the mission, shared stories, and gave people a way to engage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5nrf8pugng8wx4pvxwmg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5nrf8pugng8wx4pvxwmg.png" alt="Screenshot 2026-04-18 at 17.28.24.png" width="800" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The site had everything it needed for the moment, but it was missing the actual layers Megan was wanting like the ability to have specific animal stories showcased, specific dollar amount shown per case plus blogs and an application process for others to get on board.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I’ve Been Working On
&lt;/h2&gt;

&lt;p&gt;The current version is built with Next.js and uses Firebase for authentication and data, with Cloudinary handling image storage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzh2sloev34x9nzhsvss9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzh2sloev34x9nzhsvss9.png" alt="Screenshot 2026-04-18 at 17.29.59.png" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cases, blog posts, and site-wide content are stored in Firestore, which means updates are persisted and reflected immediately on the site. This is building the first parts of the real system Megan is looking for.&lt;/p&gt;

&lt;p&gt;That change shifts how the project can be used.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Structured data for cases, blog posts, and shared site content&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Image uploads that don’t rely on local assets&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Seeded demo data so the site is usable without setup&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s still work to do, especially around polish and edge cases, but the foundation is in a much better place than it was.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current State
&lt;/h2&gt;

&lt;p&gt;The dev version of the site is live here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://the-wolf-project-14stqkkgr-zsturmans-projects.vercel.app/" rel="noopener noreferrer"&gt;https://the-wolf-project-14stqkkgr-zsturmans-projects.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The original site is still available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yourdogcanstay.com/" rel="noopener noreferrer"&gt;https://yourdogcanstay.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s still a work in progress. There are areas that need refinement, and some features that haven’t been fully implemented yet. But it’s at a point where it’s usable and where feedback is actually helpful.&lt;/p&gt;

&lt;p&gt;If you’ve seen the story or care about the space this project is working in, there’s room to contribute, whether that’s through feedback, design suggestions, or development.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;There are a few areas I’m focused on next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Tightening up editing and data validation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Improving how donation data is handled and displayed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cleaning up remaining assumptions around static content&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Making sure the system is stable enough for real usage&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to make sure the site can actually support the work it’s tied to, without becoming something that needs constant technical maintenance.&lt;/p&gt;

</description>
      <category>dogs</category>
      <category>firebase</category>
      <category>nextjs</category>
      <category>nonprofit</category>
    </item>
    <item>
      <title>How’s My Eating?</title>
      <dc:creator>Zachary Sturman</dc:creator>
      <pubDate>Thu, 15 Aug 2024 06:17:16 +0000</pubDate>
      <link>https://dev.to/zacharysturman/hows-my-eating-39jh</link>
      <guid>https://dev.to/zacharysturman/hows-my-eating-39jh</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;“Breathe”&lt;/em&gt; &lt;/li&gt;
&lt;li&gt;&lt;em&gt;"The food’s not going anywhere."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Are you in a race?"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I eat too fast. It’s something that’s been an issue in my life for as long as I can remember. I always finish my food way faster than the people around me.&lt;/p&gt;

&lt;p&gt;Sitting in front of the TV, barely chewing my food, and missing the moment when my body tries to tell me I’m full—binge eating can be a challenging habit to break, mostly because it’s really, really hard for me to pull my concentration away from the act of eating to the act of self-control.&lt;/p&gt;

&lt;p&gt;There are a lot of benefits of slowing down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mindful eating habits lead to healthier food choices&lt;/li&gt;
&lt;li&gt;Avoiding overeating by listening to your body&lt;/li&gt;
&lt;li&gt;Quality of life increases when you actually taste what you're eating&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve tried a lot of different ways to slow down my eating like matching my bite pace with someone who eats slower and counting my chews.&lt;/p&gt;

&lt;p&gt;These strategies have their downsides, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distraction from social interactions&lt;/li&gt;
&lt;li&gt;Not allowing yourself to enjoy the flavors&lt;/li&gt;
&lt;li&gt;And it's just hard to maintain long enough to build habits&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;"There's an app for that"&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;“How’s My Eating?”&lt;/em&gt; is an app designed to provide real-time notifications about your eating habits. Specifically, the app would monitor your eating pace or chew count and alert you if you’re eating too fast. This could lead to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Healthier eating habits&lt;/li&gt;
&lt;li&gt;Better digestion&lt;/li&gt;
&lt;li&gt;More enjoyment of food&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to Make It?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To achieve this, I decided to focus on &lt;strong&gt;kinetic data&lt;/strong&gt;—movements and patterns captured by devices like AirPods. This method is less intrusive and more feasible for users who value privacy.&lt;/p&gt;

&lt;p&gt;I’m excited to continue developing &lt;em&gt;“How’s My Eating?”&lt;/em&gt; and share my progress with you. This is the first post I’ve made in this format, so I hope it came across well. I plan to develop a schedule to keep these updates coming, but for now, let’s aim for one post a week.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ios</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
