<?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.us-east-2.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>The Art of Turning a 90-Minute Task Into a 2-Month Automation Project</title>
      <dc:creator>Zachary Sturman</dc:creator>
      <pubDate>Sun, 21 Jun 2026 16:25:23 +0000</pubDate>
      <link>https://dev.to/zacharysturman/the-art-of-turning-a-90-minute-task-into-a-2-month-automation-project-4gfp</link>
      <guid>https://dev.to/zacharysturman/the-art-of-turning-a-90-minute-task-into-a-2-month-automation-project-4gfp</guid>
      <description>&lt;p&gt;For the last few years, I kept returning to the same problem in slightly different forms.&lt;/p&gt;

&lt;p&gt;I just wanted my portfolio page to update when I updated my personal projects. Or at least I wanted those two things to stay close enough together that I was not constantly re-entering the same information in multiple places. That meant titles, descriptions, status, media, links, and all the small details that make a project legible later, both to me and to anyone looking at the public-facing version of it.&lt;/p&gt;

&lt;p&gt;That was the simple version of the problem.&lt;/p&gt;

&lt;p&gt;The longer version is that I work across a lot of different kinds of projects, software, animation, art, writing, expository work, white papers, and other things that do not fit neatly into one generic portfolio template. So very quickly this stopped being just a matter of storing a title and a thumbnail somewhere. I wanted a structured way to describe projects that could survive different formats, different folders, different media types, and different stages of completion.&lt;/p&gt;

&lt;p&gt;That idea kept making sense to me, which is probably why I kept rebuilding it.&lt;/p&gt;

&lt;p&gt;What changed over time was the implementation. The stacks changed, the interfaces changed, and my understanding of the problem changed with them. Looking back, there are four versions that really define the arc: &lt;strong&gt;Obsidian + Project Management App&lt;/strong&gt;, &lt;strong&gt;OPE&lt;/strong&gt;, &lt;strong&gt;RPOVault&lt;/strong&gt;, and &lt;strong&gt;Folio&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcuhi3ne17f1hc3sgxgic.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcuhi3ne17f1hc3sgxgic.png" alt="hero.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A rough timeline&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This was not one clean straight line. A few versions overlapped, and some of them fed directly into the next without ever really being finished in the usual sense. But the broad arc looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Obsidian + Project Management App&lt;/strong&gt;: early attempt to connect projects, folders, and notes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OPE&lt;/strong&gt;: the stage where the schema and workflow became the real focus&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;RPOVault&lt;/strong&gt;: a React-based GUI layer tying the system together more directly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Folio&lt;/strong&gt;: the most complete version, a macOS app that actually shipped&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That progression matters because each version taught me something different, and the later ones only make sense in light of the earlier ones.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Obsidian + Project Management App&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fab7xbpi120fg8qcznqnv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fab7xbpi120fg8qcznqnv.png" alt="Project Metadata.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was one of the earliest versions where the problem became real software instead of just an idea.&lt;/p&gt;

&lt;p&gt;At that point I was not just thinking about a nicer way to store project metadata. I was also trying to connect the system to the way I actually worked, which meant local folders, notes, markdown, project files, and the broader context around a project. Obsidian mattered because it already held a lot of the thinking around the work. The project manager mattered because I wanted something more structured than scattered notes and folders.&lt;/p&gt;

&lt;p&gt;So this version was partly about reducing duplication, but it was also about keeping project data connected to the actual filesystem and note-taking workflow.&lt;/p&gt;

&lt;p&gt;What made this version important is that it exposed the shape of the problem pretty quickly. I was not only trying to build a project manager. I was also trying to build a way for projects, ideas, notes, snapshots, and files to live close enough together that I would not lose context or have to keep translating the same information between systems.&lt;/p&gt;

&lt;p&gt;That is also where the first real implementation tension showed up. The closer I got to the filesystem, the more useful the system felt, but also the heavier it became. Once a tool starts caring about directories, file initialization, project creation, snapshots, and integration with external note systems, it stops being a lightweight helper pretty quickly.&lt;/p&gt;

&lt;p&gt;This version did a good job of revealing that tension, even if it was not the final answer. It taught me that the real problem was not just interface design. It was the relationship between metadata and the files it was supposed to describe.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;OPE&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tjrva9piek0rxwo82u4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tjrva9piek0rxwo82u4.png" alt="Ope Hero.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OPE stands for &lt;strong&gt;Organize, Prioritize and Execute&lt;/strong&gt;, and this was probably the point where the system became most conceptually clear to me.&lt;/p&gt;

&lt;p&gt;The key idea behind OPE was that this was not supposed to be one giant app doing everything in one flat space. It was meant to be three different systems working as a pipeline for moving projects through a process.&lt;/p&gt;

&lt;p&gt;That framing mattered. It meant I was thinking less about a single dashboard and more about how a project should move from raw state to structured state to active state. In other words, not every item needed the same treatment at the same time. Some things needed organizing. Some needed prioritization. Some were ready for execution.&lt;/p&gt;

&lt;p&gt;This was also the stage where _project.json really became central.&lt;/p&gt;

&lt;p&gt;I liked _project.json because it was a simple, visible convention. If every project folder had a predictable metadata file, I could find those files easily, parse them, filter them, and use them elsewhere. That gave me a shared structure without requiring everything to live inside one opaque database. It also kept the metadata local to the project itself, which still feels like a strong instinct to me.&lt;/p&gt;

&lt;p&gt;The problem, of course, is that once _project.json becomes the center of the system, editing it becomes part of the workflow. That was the point where the elegant idea started creating its own overhead. Manual JSON editing was technically possible, but it also introduced exactly the kind of friction and human error I was trying to avoid. So the schema solved one problem and created another.&lt;/p&gt;

&lt;p&gt;Still, OPE was important because it clarified what was actually durable in all of this. The durable part was not the shell. It was the structured project record and the idea that projects move through stages, not just sit in folders.&lt;/p&gt;

&lt;p&gt;It also clarified something else. I was not really building one thing. I was building adjacent systems and trying to make them cooperate. That was a useful insight, even if the resulting machinery was already getting pretty ambitious.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;RPOVault&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;RPOVault was, in a lot of ways, the next obvious move.&lt;/p&gt;

&lt;p&gt;Once I had a metadata-centered system, and once I knew manual editing was becoming a problem, it made sense to put a GUI in front of it. RPOVault was basically the same general idea, but with a React interface tying everything together more directly.&lt;/p&gt;

&lt;p&gt;That shift mattered because it changed the day-to-day experience of the system. Instead of thinking first in terms of raw files and then editing around them, I could think in terms of fields, forms, views, and the structure of the data as a user-facing editor. In theory, that should have reduced friction and made the whole thing more reliable.&lt;/p&gt;

&lt;p&gt;In practice, it did help, but it also revealed that better editing does not remove architectural complexity. It just changes where you feel it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9xv0q53tk1gq3r807os2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9xv0q53tk1gq3r807os2.png" alt="Content json.png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The schema still had to be coherent. File paths still had to work. Media still had to land in the right place. The system still had to preserve structure across saves and changes. So while the GUI made the system friendlier, it did not fundamentally make it lighter.&lt;/p&gt;

&lt;p&gt;That was one of the more useful lessons for me. Sometimes a cleaner interface does solve the problem. Sometimes it only makes the underlying complexity easier to look at.&lt;/p&gt;

&lt;p&gt;RPOVault sits in that middle ground for me. It was a meaningful step because it proved the value of editing the metadata through a proper GUI, but it also made it harder to ignore that the overall system was accumulating a lot of responsibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Folio&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqlt7a2ozp5hzu1b81hc6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqlt7a2ozp5hzu1b81hc6.png" alt="Folio Launch Screen.png" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Folio was the version where the project became real enough to ship.&lt;/p&gt;

&lt;p&gt;By this point the broad shape of the system had settled. I still wanted a structured project record. I still wanted one place to manage project and portfolio metadata. I still wanted the public-facing portfolio to stay close to the actual project data. The difference was that Folio packaged all of that into a macOS app that I could actually use end to end.&lt;/p&gt;

&lt;p&gt;This was the most complete version, and the one that proved to me that the whole idea was technically possible.&lt;/p&gt;

&lt;p&gt;It also taught me the most because it pushed all the tradeoffs into the open.&lt;/p&gt;

&lt;p&gt;The app worked. I got the metadata flow working end to end. I shipped it. It is on the App Store now as Folio Studio. But by the time I had that result, I also understood what it was costing me to maintain. The fragile parts were still fragile, especially file paths and media. The sandbox system never got to a place I fully trusted. The input cost was still higher than I wanted. And once you are maintaining a custom app that sits in the middle of a project-ingest and portfolio workflow, every bug matters more because the system has become part of the path the work has to travel through.&lt;/p&gt;

&lt;p&gt;That was probably the clearest turning point.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkviofs9awek2oektcaqd.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkviofs9awek2oektcaqd.png" alt="Folio Media .png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A tool that is supposed to help with project ingest can quietly become a roadblock preventing projects from being ingested. Once I could feel that happening, it was hard to ignore. If the cost of keeping the system correct starts outweighing the value of the system itself, then the design may still be interesting, but the workflow is already in trouble.&lt;/p&gt;

&lt;p&gt;I do not say that as a criticism of the project. Folio taught me a lot, and I do not regret building it. It gave me a much clearer understanding of data modeling, editor design, workflow boundaries, and the practical cost of trying to own every layer yourself.&lt;/p&gt;

&lt;p&gt;It also taught me when to stop.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;What survived all four versions&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fevaui2ylmlh0uskkj4w9.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fevaui2ylmlh0uskkj4w9.png" alt="Foli problemt space.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Across these versions, the project record was the part that consistently remained, because the core need was not a specific shell, stack, or interface so much as a clearer structure for the project itself.&lt;/p&gt;

&lt;p&gt;A serious project usually needs a title, yes, but also status, descriptions, media, references, relationships, context, and enough consistency that it can be reused elsewhere without re-entering everything manually.&lt;/p&gt;

&lt;p&gt;That part still feels true to me.&lt;/p&gt;

&lt;p&gt;What changed is my view of how much one custom system should be responsible for.&lt;/p&gt;

&lt;p&gt;For a while I kept assuming the right answer was one system. One place to edit metadata, manage project context, hold media, keep local files connected, and feed the portfolio. Sometimes that instinct produced something useful. Other times it produced a bigger and bigger machine around a workflow that did not actually happen often enough to justify the maintenance.&lt;/p&gt;

&lt;p&gt;That is where the philosophy shifted.&lt;/p&gt;

&lt;p&gt;What I finally understood was that building your own engine gives you a lot of control and teaches you a lot, but it is a lot of extra work and time when you only plan on driving it once a year.&lt;/p&gt;

&lt;p&gt;I do not think building custom tools was a mistake. It was useful, interesting, and educational. The mistake would be deciding that because I learned a lot the first time, I should keep repeating the same maintenance burden even after the tradeoff stopped making sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Where I am now&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqgs84yzvs8rrex1pwc6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqgs84yzvs8rrex1pwc6.png" alt="tech complexity.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The ending here is not that I gave up on the idea. It is that I changed how I build it.&lt;/p&gt;

&lt;p&gt;These days, Notion is the source of truth for project metadata. Linear handles task management. n8n initiates the automation layer. GitHub Actions handles build and testing. Firebase handles hosting.&lt;/p&gt;

&lt;p&gt;The workflow now looks more 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 project metadata

→ n8n automation

→ transform / package data

→ GitHub Actions for build and test

→ Firebase hosting

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

&lt;/div&gt;



&lt;p&gt;This setup does not give me the same feeling of total control that the custom app did, but it does solve the actual problem more effectively. I can keep project metadata in one place, automate the parts that benefit from automation, and avoid maintaining a custom metadata generator that introduces more overhead than it removes.&lt;/p&gt;

&lt;p&gt;Some things are still manual, and I want them to stay that way. I still enter information in Notion myself. I still build and push the portfolio manually because I want to visually inspect added work before it goes live. That is not a missing feature. That is the point. I no longer think every manual step is a flaw.&lt;/p&gt;

&lt;p&gt;That distinction matters more to me now than it used to.&lt;/p&gt;

&lt;p&gt;For a while I was trying to remove all friction from the workflow, but some friction is useful when it creates a deliberate review step. The real problem was duplication, not human involvement. Once I understood that more clearly, the architecture got simpler.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;What I learned&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flhhf4uk57wbgn48m9g1p.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flhhf4uk57wbgn48m9g1p.png" alt="Productivity overload.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I learned a lot from this, and most of it came from building the whole thing far enough to see where it broke down.&lt;/p&gt;

&lt;p&gt;I learned that structured project metadata is still a good idea, but the editing and maintenance cost matters just as much as the schema itself.&lt;/p&gt;

&lt;p&gt;I learned that file paths and media are exactly the kinds of details that can quietly make a system brittle, especially when the system is trying to stay close to real project folders.&lt;/p&gt;

&lt;p&gt;I learned that a GUI can make a system easier to use without actually reducing its architectural weight.&lt;/p&gt;

&lt;p&gt;I learned that a tool meant to help ingest projects can become a roadblock if it asks too much of the work before the work is allowed in.&lt;/p&gt;

&lt;p&gt;And I learned that sometimes time and effort are better spent integrating existing tools together than building one more custom layer yourself.&lt;/p&gt;

&lt;p&gt;The part I value more now is not just the control that comes from building your own system. It is also the open source community and the current ecosystem of AI and automation tools that make integration far easier than it used to be. More and more, the useful work is not building every component yourself. It is deciding what deserves to be custom and what should just be connected.&lt;/p&gt;

&lt;p&gt;I finished Folio. I shipped it. I do not plan to maintain it or extend it anymore. That is not me dismissing the project. It is me recognizing that I got what I needed from it, and that continuing would mostly mean paying the same maintenance bill again.&lt;/p&gt;

&lt;p&gt;That feels like a much healthier place to stop than some other projects I have held onto for too long in the past.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;If you want the deeper version&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I wrote more specifically about the current system and how I use it now.&lt;/p&gt;

&lt;p&gt;If you want to see how I sync my portfolio using Notion, read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&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;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see how I use Notion to track my projects, read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a more in-depth look at the original app and code, or want to use any of it yourself, the repository is here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ZSturman/Folio-Studio" rel="noopener noreferrer"&gt;https://github.com/ZSturman/Folio-Studio&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>automation</category>
      <category>notetaking</category>
      <category>notion</category>
      <category>obsidian</category>
    </item>
    <item>
      <title>Consolidating an Offline-First Episodic Memory System</title>
      <dc:creator>Zachary Sturman</dc:creator>
      <pubDate>Sun, 21 Jun 2026 16:24:45 +0000</pubDate>
      <link>https://dev.to/zacharysturman/consolidating-an-offline-first-episodic-memory-system-1cjc</link>
      <guid>https://dev.to/zacharysturman/consolidating-an-offline-first-episodic-memory-system-1cjc</guid>
      <description>&lt;p&gt;There is a point in some projects where separate experiments stop feeling separate, and start revealing themselves as parts of the same system.&lt;/p&gt;

&lt;p&gt;That is where this project is now.&lt;/p&gt;

&lt;p&gt;What began as several lines of work around episodic memory, working memory, panorama-based location identification, observability, notebooks, and dashboard inspection is starting to converge into one canonical project: a single offline-first episodic memory system with one runtime, one artifact contract, and one coherent way to inspect what happened.&lt;/p&gt;

&lt;p&gt;This is not a finished system. It is still very much a work in progress, and that is part of why I wanted to write this now.&lt;/p&gt;

&lt;p&gt;I wanted to document not only what the project currently is, but also how I am thinking about it, what I decided to merge, what I chose to defer, what seems most important right now, and why I keep coming back to the idea that location and whereabouts are central to building useful memory systems.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcnhg8ui1fny7miiqdovl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcnhg8ui1fny7miiqdovl.png" alt="High level arch.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Current Shape of the Project
&lt;/h2&gt;

&lt;p&gt;The project is now converging around one canonical Python package, &lt;code&gt;episodic_memory&lt;/code&gt;, and one canonical web app in &lt;code&gt;dashboard/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At a practical level, that means I am no longer treating the root package, the working-memory subsystem, and the legacy episodic-memory-agent as separate active products. Instead, I am absorbing the useful parts into one architecture.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;one canonical Python package at &lt;code&gt;src/episodic_memory/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;one canonical Next.js dashboard in &lt;code&gt;dashboard/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;one notebook workspace in &lt;code&gt;notebooks/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;archived legacy systems in &lt;code&gt;archive/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important change is not really the directory layout. It is the conceptual shift. I want one project with one truth surface, rather than several parallel implementations carrying overlapping ideas.&lt;/p&gt;

&lt;p&gt;The merged project is offline-first and organized around a single run artifact bundle. That bundle is what the notebooks read, what the inspector API serves, and what the dashboard renders. In other words, analysis and UI are peers over the same persisted data, rather than two disconnected interpretations of runtime state.&lt;/p&gt;

&lt;p&gt;That feels like the right foundation.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8dsjr7k26e0o7yv1blqt.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8dsjr7k26e0o7yv1blqt.png" alt="repo structure.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the System Does Right Now
&lt;/h2&gt;

&lt;p&gt;At the moment, the canonical runtime supports two active modes end to end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;stub mode&lt;/strong&gt;, for deterministic scenario execution&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;panorama mode&lt;/strong&gt;, for image or video exploration tied to location identification&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every run produces one canonical bundle containing files such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;manifest.json&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;summary.json&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;memories.json&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;graph.json&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;events.jsonl&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;memory_decisions.jsonl&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;steps/step_XXXX.snapshot.json&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That bundle is the contract.&lt;/p&gt;

&lt;p&gt;This matters a lot to me because one of the recurring problems in experimental cognitive systems is that the runtime may be doing interesting things, but the inspection surface is brittle, partial, or too tightly coupled to internals. I want the opposite. I want runs to be inspectable after the fact, replayable in a meaningful way, and transparent enough that I can understand not just what the system concluded, but why.&lt;/p&gt;

&lt;p&gt;So this project is as much about memory transparency as it is about memory itself.&lt;/p&gt;

&lt;p&gt;Each canonical step snapshot records things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;location hypotheses&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;match evaluation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;evidence bundle&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;memory delta&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;graph state&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;candidate ideas&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;writeback candidates&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;write decisions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;node lifecycle changes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;debug records&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means the project is not just building memory. It is building an inspectable trace of memory formation, and also of memory refusal.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why I Merged Everything Into One Canonical Project
&lt;/h2&gt;

&lt;p&gt;One of the biggest decisions I made was to stop preserving the old boundaries as if they were permanent.&lt;/p&gt;

&lt;p&gt;At one point, I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;root cognition, working-memory, and transparency work&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a separate working-memory subsystem identity&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a legacy panorama and location-identification codebase&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;separate inspection habits across notebooks and dashboard&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That structure made sense while the ideas were still exploratory. Over time, though, it started creating the wrong incentives. There was too much translation, too many implied contracts, and too many places where one surface knew something another surface did not.&lt;/p&gt;

&lt;p&gt;So the decision became straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;one active package, one active dashboard, one artifact model, one pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a clean break. It is not a compatibility-first architecture. It is a coherence-first architecture.&lt;/p&gt;

&lt;p&gt;That also meant making a few related decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;old public package names should not remain as long-term compatibility shims&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the working-memory subsystem should be absorbed, not preserved as a separate installable identity&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the legacy episodic-memory-agent should be archived once the useful ideas and import paths are absorbed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unity should remain part of the long-term plan, but not be required for the first merged version&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last decision matters more than it may seem.&lt;/p&gt;

&lt;p&gt;A project like this can easily become a hostage to future ambition. I do not want Unity integration, embodied simulation, or future interaction loops to block progress on the core questions. So I am preserving the architectural slot for Unity, but explicitly deferring it in v1.&lt;/p&gt;

&lt;p&gt;That keeps the current work honest: build the canonical memory runtime first, make it observable, make it testable, make it inspectable, then expand.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzoqogerl96p5q795laq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzoqogerl96p5q795laq.png" alt="software ingest.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Idea I Keep Returning To: Location Matters
&lt;/h2&gt;

&lt;p&gt;A major influence on this project is the Thousand Brains Project and Monty. Their work is grounded in the idea that intelligence is sensorimotor, structured, and deeply tied to how repeated cortical units model the world. Monty is presented as an implementation of a thousand-brains system, with a strong connection to cortical columns as repeating computational units.&lt;/p&gt;

&lt;p&gt;I think that framing is directionally right.&lt;/p&gt;

&lt;p&gt;More specifically, I think their emphasis on cortical columns, sensorimotor learning, and structure over unstructured pattern matching points toward something important. Their work resonates with how I have been thinking about this space.&lt;/p&gt;

&lt;p&gt;Where I keep focusing, though, is on location and whereabouts as an especially important part of the system.&lt;/p&gt;

&lt;p&gt;Not necessarily as the first step in a strict sequence, and maybe not even as a separate step at all.&lt;/p&gt;

&lt;p&gt;I think of it more as a concurrent process that develops alongside object recognition and pose estimation.&lt;/p&gt;

&lt;p&gt;That is increasingly how I see it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;object&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;pose&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;location&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;relation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;revisitation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;memory update&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These may not be serial modules so much as mutually constraining signals.&lt;/p&gt;

&lt;p&gt;When a system is trying to determine what it is looking at, it is also learning where it is, what viewpoint it has, what changed since the last encounter, and whether the current scene matches a stable memory or represents something novel. In practice, location and object understanding seem tightly entangled.&lt;/p&gt;

&lt;p&gt;That is one reason this project currently emphasizes panorama and location-memory behavior. The system scans, extracts features, builds embeddings, compares them to stored location memories, and then decides whether it is revisiting something known or encountering something new enough to justify a memory update.&lt;/p&gt;

&lt;p&gt;That may sound narrower than “general intelligence,” but I do not think it is narrow in the wrong way. I think it is one of the most grounding places to start.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fncjaa4v0ez603ti4chfy.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fncjaa4v0ez603ti4chfy.png" alt="1.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Way I Am Thinking About It So Far
&lt;/h2&gt;

&lt;p&gt;One thing I have become more convinced of is that useful memory systems should not be treated as a black box sitting after perception.&lt;/p&gt;

&lt;p&gt;Memory has to be part of the interpretation pipeline itself.&lt;/p&gt;

&lt;p&gt;That has led me back to a few recurring principles.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. One pipeline should handle multiple input modes
&lt;/h3&gt;

&lt;p&gt;Stub scenarios and panorama exploration should not become separate worlds. They should normalize into the same observation and evidence model, then flow through one shared pipeline.&lt;/p&gt;

&lt;p&gt;That shared pipeline should cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;perception or input normalization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;location matching&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;working-memory update&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;write-decision evaluation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;artifact emission&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the system easier to reason about, and it makes cross-mode comparison possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Memory should be inspectable, not merely stored
&lt;/h3&gt;

&lt;p&gt;A saved memory without context is not enough.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;what candidates existed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;what evidence was considered&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;what was skipped&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;what was written&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;how graph state changed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;what confidence or match structure led to the decision&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So transparency is not a side feature here. It is part of the architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Jupyter and the dashboard should inspect the same reality
&lt;/h3&gt;

&lt;p&gt;I do not want notebooks to be the “real” place where understanding happens while the dashboard becomes a thinner or less truthful surface. I also do not want the opposite.&lt;/p&gt;

&lt;p&gt;The notebooks and dashboard should be peers over one persisted artifact contract.&lt;/p&gt;

&lt;p&gt;That is why the dashboard reads through a file-backed inspector API rather than importing arbitrary runtime state directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Legacy work should remain explorable without staying active
&lt;/h3&gt;

&lt;p&gt;Some of the older work still matters, but that does not mean it should stay in the active install path forever.&lt;/p&gt;

&lt;p&gt;Instead of preserving old systems as living parallel products, I want import paths that convert legacy runs into the canonical bundle format. That way old runs remain inspectable without forcing the project to carry multiple active architectures indefinitely.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Unity matters, but it is not the bottleneck right now
&lt;/h3&gt;

&lt;p&gt;Embodiment matters. Simulation matters. Interaction matters.&lt;/p&gt;

&lt;p&gt;But right now the bottleneck is not whether the system can run in Unity. The bottleneck is whether I have a canonical memory architecture that is inspectable and stable enough to justify a richer embodiment layer.&lt;/p&gt;

&lt;p&gt;So Unity remains on the architectural horizon, but not in the critical path for this version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Has Already Been Decided
&lt;/h2&gt;

&lt;p&gt;A lot of the project is still fluid, but some decisions now feel firm enough to state clearly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The canonical public surface
&lt;/h3&gt;

&lt;p&gt;The project’s public Python identity is &lt;code&gt;episodic_memory&lt;/code&gt;, and the CLI is &lt;code&gt;episodic-memory&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That may sound small, but naming matters. It signals where the center of gravity is now.&lt;/p&gt;

&lt;h3&gt;
  
  
  The canonical layout
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;episodic_memory.inputs&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;episodic_memory.pipeline&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;episodic_memory.memory&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;episodic_memory.artifacts&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;episodic_memory.inspector&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;episodic_memory.wm&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the architecture I want to build on top of, rather than around.&lt;/p&gt;

&lt;h3&gt;
  
  
  The canonical execution model
&lt;/h3&gt;

&lt;p&gt;The execution model is intentionally straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;an input mode produces normalized observation data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the working-memory engine updates graph state and decision traces&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the location-memory store maintains records, matches, and deltas&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a canonical step snapshot is emitted&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the artifact writer persists the run bundle&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;notebooks, the inspector API, and the dashboard inspect those files&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the project in one sentence.&lt;/p&gt;

&lt;h3&gt;
  
  
  The canonical notebooks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;pipeline workspace&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;memory explorer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;run comparison and dashboard parity&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not side utilities. They are part of the development surface of the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  The dashboard scope
&lt;/h3&gt;

&lt;p&gt;The dashboard is read-only by design right now. It exists to inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;run summaries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;timeline&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;memories&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;graph and topology&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;evidence and matches&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;save and skip decisions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is an inspection system, not yet a control room.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Still Unresolved
&lt;/h2&gt;

&lt;p&gt;Because this is still a work in progress, it helps to be explicit about what is not finished.&lt;/p&gt;

&lt;p&gt;I am still working through questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;how rich the memory-detail rendering should become&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;how best to compare bundles for regression analysis&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;how much panorama-specific interpretability should be preserved directly in the dashboard&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;how to calibrate match behavior across more varied scenes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;how much graph state is enough for transparency without overwhelming the inspection surface&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;how Unity should eventually plug in without distorting the canonical artifact contract&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a deeper unresolved question underneath the implementation work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should count as a memory unit in a system like this?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Is it a location?&lt;/p&gt;

&lt;p&gt;A revisitable scene?&lt;/p&gt;

&lt;p&gt;A graph substructure?&lt;/p&gt;

&lt;p&gt;An object-place relation?&lt;/p&gt;

&lt;p&gt;A sensorimotor hypothesis that proved stable enough to retain?&lt;/p&gt;

&lt;p&gt;I do not think I have a final answer yet. What I do know is that I want the system to preserve enough evidence and enough decision history that I can move toward that answer empirically, rather than only philosophically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Offline-First
&lt;/h2&gt;

&lt;p&gt;I keep returning to offline-first design because it imposes a discipline that research systems often avoid.&lt;/p&gt;

&lt;p&gt;If the system has to produce a persisted run bundle that stands on its own, then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the contract has to be real&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the evidence has to be portable&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the inspector has to be decoupled&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the dashboard has to render from artifacts, not hidden runtime assumptions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;notebooks have to operate on the same saved outputs as everything else&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, offline-first architecture forces honesty.&lt;/p&gt;

&lt;p&gt;That honesty is valuable for debugging, for comparison, for importing older runs, and eventually for any serious evaluation workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Is Going Next
&lt;/h2&gt;

&lt;p&gt;The longer-term direction is fairly clear, even if the exact path is still changing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Near term
&lt;/h3&gt;

&lt;p&gt;The immediate goal is to stabilize the canonical stub and panorama modes, keep notebooks and dashboard aligned on the same artifact contract, and improve panorama matching calibration across more varied scenes.&lt;/p&gt;

&lt;p&gt;That is the practical next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Medium term
&lt;/h3&gt;

&lt;p&gt;The next layer after that is richer memory-detail rendering, stronger bundle diff tooling, and better regression analysis across runs.&lt;/p&gt;

&lt;p&gt;That is where the system becomes not just inspectable, but comparable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Longer term
&lt;/h3&gt;

&lt;p&gt;Longer term, I want to reintroduce Unity through a canonical adapter slot while preserving the same bundle and inspector surfaces.&lt;/p&gt;

&lt;p&gt;That matters because I do not want embodiment support to split the architecture back apart. If Unity returns, it should return as another producer of canonical runs, not as a competing universe.&lt;/p&gt;

&lt;p&gt;Beyond the concrete roadmap is the broader research direction behind all of this.&lt;/p&gt;

&lt;p&gt;I want to keep exploring whether episodic memory, location memory, working memory, and perceptual inference can be brought closer together in a system that feels more biologically grounded without becoming vague.&lt;/p&gt;

&lt;p&gt;That is also why I keep returning to Thousand Brains and Monty. Their framing of intelligence as sensorimotor and structurally grounded continues to feel important to me, especially the emphasis on cortical columns and distributed modeling.&lt;/p&gt;

&lt;p&gt;My own emphasis, at least in this phase, is that location and whereabouts should be treated as first-class signals in that story. Not as an afterthought, and not as metadata, but as part of how the system decides what it is encountering and whether that encounter should become memory.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpgra8spqlmbj7jyuet0u.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpgra8spqlmbj7jyuet0u.png" alt="Runtime vs artifact.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Am Publishing This Before It Is Done
&lt;/h2&gt;

&lt;p&gt;Because this project is not just a codebase. It is also an evolving theory of what matters in memory-centric AI systems.&lt;/p&gt;

&lt;p&gt;Publishing while it is still unfinished does two useful things for me.&lt;/p&gt;

&lt;p&gt;First, it forces me to state the architecture clearly enough that I can see whether it actually holds together.&lt;/p&gt;

&lt;p&gt;Second, it preserves the reasoning behind the project while that reasoning is still active and changing.&lt;/p&gt;

&lt;p&gt;Too many project writeups happen after the fact and make the final architecture look inevitable. That is almost never true. In this case, the actual story is messier, and I think more useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;multiple active threads of work&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a decision to unify them&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a belief that transparency matters&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a growing conviction that location is central&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a deliberate choice not to let future embodiment concerns block present architectural coherence&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the real work in progress.&lt;/p&gt;

&lt;p&gt;And that is the part I most want documented.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqmk8ztm1b2stybhxamy.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqmk8ztm1b2stybhxamy.png" alt="Sketch ideas.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;So this is where the project stands right now:&lt;/p&gt;

&lt;p&gt;a single canonical episodic memory system, still under construction, organized around one package, one dashboard, one artifact contract, and one increasingly clear idea, that memory, location, evidence, and working state should belong to the same inspectable system.&lt;/p&gt;

&lt;p&gt;I do not think this is the final form of the project.&lt;/p&gt;

&lt;p&gt;But I do think it is the right consolidation.&lt;/p&gt;

&lt;p&gt;And I think getting this part right, especially the handling of location, revisitation, evidence, and write decisions, will make everything that comes later more grounded.&lt;/p&gt;

&lt;p&gt;That includes richer perception.&lt;/p&gt;

&lt;p&gt;That includes object and pose integration.&lt;/p&gt;

&lt;p&gt;That includes Unity.&lt;/p&gt;

&lt;p&gt;And that includes any larger theory about how episodic systems should work.&lt;/p&gt;

&lt;p&gt;For now, this is the work: make the memory system coherent, transparent, and real enough to inspect.&lt;/p&gt;

&lt;p&gt;Everything else can build on that.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2043ygo0szxsfa75grg.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2043ygo0szxsfa75grg.png" alt="publis api.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Explore the Repository
&lt;/h2&gt;

&lt;p&gt;If you want to see the project itself, the repository is here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repo:&lt;/strong&gt; &lt;a href="https://github.com/ZSturman/Episodic-Memory-Agent" rel="noopener noreferrer"&gt;https://github.com/ZSturman/Episodic-Memory-Agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The codebase reflects the same transition described in this post, from separate lines of work toward one more coherent episodic memory architecture. That context may make the repository easier to navigate, especially if you are interested in the runtime, artifact structure, or inspection surfaces.&lt;/p&gt;

&lt;p&gt;If you want the earlier, narrower stepping stone into this same line of work, I wrote about that here: &lt;a href="https://zachary-sturman.com/articles/building-a-location-first-learning-agent-to-explore-context-memory-and-consciousness" rel="noopener noreferrer"&gt;https://zachary-sturman.com/articles/building-a-location-first-learning-agent-to-explore-context-memory-and-consciousness&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>dashboard</category>
      <category>episodicmemory</category>
      <category>monty</category>
      <category>syntheticmemory</category>
    </item>
    <item>
      <title>Building ChewSense - Using Motion Data For An On-Device AI Health Agent</title>
      <dc:creator>Zachary Sturman</dc:creator>
      <pubDate>Sun, 21 Jun 2026 16:24:42 +0000</pubDate>
      <link>https://dev.to/zacharysturman/building-chewsense-using-motion-data-for-an-on-device-ai-health-agent-57d5</link>
      <guid>https://dev.to/zacharysturman/building-chewsense-using-motion-data-for-an-on-device-ai-health-agent-57d5</guid>
      <description>&lt;p&gt;I did not build ChewSense in a straight line, and I definitely did not build it efficiently.&lt;/p&gt;

&lt;p&gt;What I built, especially at the beginning, was a chain of experiments that kept circling the same question from different angles: could I use motion data from AirPods to detect chewing in a way that was actually useful? That question stayed pretty consistent, but almost everything around it changed. The app names changed, the architecture changed, the scope changed, and my idea of what mattered changed too.&lt;/p&gt;

&lt;p&gt;At first I thought this was going to be a model problem. Get the right signals, train something smart, export it, and then I would be most of the way there. That was not really true. The longer I worked on it, the more I realized the hard part was everything around the model: collecting good sessions, syncing motion with video, labeling data in a way I could trust later, deciding what not to build, and getting the on-device pipeline to match the training pipeline closely enough that the results meant anything.&lt;/p&gt;

&lt;p&gt;That shift in understanding is probably the real story of this project.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1soeeqe43fvqzq8x53xm.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1soeeqe43fvqzq8x53xm.png" alt="34.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  I started by trying to prove the sensors were even worth using
&lt;/h2&gt;

&lt;p&gt;The earliest version of this work was very close to the hardware. I was recording motion and audio, looking at raw traces, and trying to get comfortable with the signal itself. At that point I was not building a real product. I was mostly trying to answer smaller questions first.&lt;/p&gt;

&lt;p&gt;Can I read the motion data reliably?&lt;/p&gt;

&lt;p&gt;Can I record it in real time?&lt;/p&gt;

&lt;p&gt;Can I look at it and develop any intuition for what is happening?&lt;/p&gt;

&lt;p&gt;Can I collect more than one modality at once without the whole thing turning brittle?&lt;/p&gt;

&lt;p&gt;That phase was useful, but it was also the beginning of one of my recurring mistakes, which was treating optional complexity like progress. I was exploring motion and audio together, thinking about a bunch of sensing paths at once, and leaving the door open to all kinds of possibilities before I had really earned that breadth. Some of that exploration helped, but some of it was just me making the project bigger because I had not yet learned which constraints were actually helpful.&lt;/p&gt;

&lt;p&gt;Still, I do not think that early phase was wasted. It taught me something important that shaped the rest of the project: if you spend time with raw signals early, you make better decisions later. You stop treating the data as an abstraction and start treating it like a material with quirks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I started framing it like a product before I had really built the system
&lt;/h2&gt;

&lt;p&gt;Once the project had a name and a clearer concept, it started looking more like an app idea and less like a sensor experiment. That was the Hows My Eating phase, and in some ways it was useful because it forced me to articulate why any of this should exist in the first place.&lt;/p&gt;

&lt;p&gt;I was no longer just asking whether AirPods motion data could be read. I was asking whether it could support something like chew counting, pace detection, or behavior feedback around eating. That made the project more legible, but it also revealed another thing I tend to do when I get interested in a problem: I widen the ambition faster than the underlying system deserves.&lt;/p&gt;

&lt;p&gt;At different points I was thinking about audio, accelerometer data, synchronized video as ground truth, chew counting, pace classification, maybe custom hardware, maybe desktop tools, maybe more advanced labeling flows. Looking back, a lot of that was me trying to solve the whole category instead of solving the next real bottleneck.&lt;/p&gt;

&lt;p&gt;I do not say that as self-criticism for the sake of it. I think this is actually one of the main lessons from the project. Early on, I was still learning the difference between a broad concept and a buildable system. A concept can afford to include everything. A real system has to start saying no.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project became real once I started collecting sessions instead of just streams
&lt;/h2&gt;

&lt;p&gt;The first major change was when I moved from just capturing sensor data to building something that treated recordings as sessions. That was a much bigger step than it sounds.&lt;/p&gt;

&lt;p&gt;Once I was recording AirPods motion alongside video, the project changed shape immediately. Now I had to care about synchronization, storage, export, review, and whether a session was complete enough to be useful later. The moment video entered the loop as ground truth, I was no longer just collecting data. I was building the first version of a dataset.&lt;/p&gt;

&lt;p&gt;That also changed what “working” meant. Before that, working meant I could see the signal. After that, working meant I could trust the session enough to use it again. Those are very different standards.&lt;/p&gt;

&lt;p&gt;I think this was the point where the project stopped being an interesting technical experiment and started becoming an ML workflow, even if I did not fully appreciate that yet.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6jjmg0eim3scu4dm22eu.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6jjmg0eim3scu4dm22eu.png" alt="6.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Labeling turned out to be one of the real projects inside the project
&lt;/h2&gt;

&lt;p&gt;This was probably one of the most important things I learned.&lt;/p&gt;

&lt;p&gt;It is very easy to say, at a high level, that video can provide ground truth for motion data. That sounds clean when you say it quickly. In practice, it turns into a whole operational problem. You need imports, validation, synchronization checks, labeling rules, export formats, and enough structure that you can come back later and still believe your own dataset.&lt;/p&gt;

&lt;p&gt;I ended up building and exploring multiple labeling paths, including app-based flows, Python workflows, and even desktop-oriented ideas. At first that sounds like overkill, and some of it probably was. But it also reflected a real problem: once I had synchronized motion and video, labeling became one of the hardest parts of the entire system.&lt;/p&gt;

&lt;p&gt;This is where I started learning more serious tradeoffs.&lt;/p&gt;

&lt;p&gt;I learned that building everything from scratch is usually less impressive after the third week than it sounds on day one. Native systems and simpler file-based workflows often get you farther, especially when the job is not to invent a novel interface but to preserve data integrity. I also learned that labeling is not just support work. In a project like this, labeling shapes the dataset, the model, and the product direction at the same time.&lt;/p&gt;

&lt;p&gt;Every time the labeling workflow changed, the project changed with it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftc2qu7q7g3e2q8hvd69x.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftc2qu7q7g3e2q8hvd69x.png" alt="5.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  I spent a while exploring too many branches, but that taught me what to narrow
&lt;/h2&gt;

&lt;p&gt;By 2025 there were clearly parallel branches in the work. Some of them were directly useful, and some of them were me still learning where the boundary of the project should be.&lt;/p&gt;

&lt;p&gt;I explored video-based and landmark-assisted approaches, Python pipelines for parsing and review, smaller prototype apps, more product-like recorder designs, and different ways to organize sessions and metadata. From the outside that might look messy, and it was messy, but it was also how I learned what the project actually needed.&lt;/p&gt;

&lt;p&gt;One of the strongest lessons from that phase was that the final runtime model did not need to carry all of the complexity that the development process needed. Video turned out to be incredibly valuable for collection, validation, and labeling, even if the model itself was going to lean much more heavily on motion. That distinction mattered a lot.&lt;/p&gt;

&lt;p&gt;I also learned something more encouraging on the modeling side: motion-data models for this kind of problem do not need to be huge. Once I had a cleaner feature pipeline, it became clear that a relatively small model could plausibly do useful work here. That changed my expectations. I stopped thinking in terms of bigger models and started thinking much more about better sessions, better labels, and better feature parity between training and inference.&lt;/p&gt;

&lt;p&gt;That was a healthier way to think about it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fud2l1tj66qxp1ft625f4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fud2l1tj66qxp1ft625f4.png" alt="234.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The collector became better when I stopped pretending it needed to be the final product
&lt;/h2&gt;

&lt;p&gt;One of the more useful turning points was when the data collection app got more disciplined and more honest about what it was.&lt;/p&gt;

&lt;p&gt;Earlier versions were still carrying some product-story weight. Later versions were much more comfortable being research and dataset tools. They recorded synchronized video and motion, stored files in a way I could inspect, supported labeling, handled export, and paid more attention to data integrity. That clarity helped a lot.&lt;/p&gt;

&lt;p&gt;I think I had been blending two jobs together for too long: building the thing that collects trustworthy data, and building the thing a user would eventually experience as a product. Those are related, but they are not the same. Once I stopped forcing them into the same shape, the project got better.&lt;/p&gt;

&lt;p&gt;This is where I also learned to appreciate more native approaches. File-system-based organization, direct CSV storage, simpler review paths, and more explicit session folders ended up being more useful than trying to make the whole thing feel abstracted and product-like too early. In this case, native systems were not a compromise. They were part of what made the pipeline more believable.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foycbmxdguzv8gfthzq1s.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foycbmxdguzv8gfthzq1s.png" alt="7.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The ML pipeline finally gave the project a backbone
&lt;/h2&gt;

&lt;p&gt;Once the collection and labeling story got stronger, the modeling work became much more grounded.&lt;/p&gt;

&lt;p&gt;The pipeline became much more explicit: validate and transform the data, extract features from sliding windows, train a small model, export normalization stats, convert to CoreML, and test the result in an on-device setting. That kind of narrowing was hard-earned. Earlier versions of the project had lots of possible sensing ideas. This was the point where I started saying: these are the inputs, this is the feature set, this is the model, and this is the path to deployment.&lt;/p&gt;

&lt;p&gt;That felt like progress, but not because it was flashy. It felt like progress because it was constrained.&lt;/p&gt;

&lt;p&gt;I also learned a lot here about the science of jaw and eating motion, or at least enough to stop treating it casually. Temporal windows mattered. Signal magnitude by itself was not enough. Variance, jerk, zero crossings, and frequency-related information all helped tell a more meaningful story about what was happening. The model was small, but the feature design carried a lot of the intelligence.&lt;/p&gt;

&lt;p&gt;That was another useful lesson. A lot of the work in practical ML is not choosing a complicated model. It is understanding the phenomenon well enough to represent it cleanly.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgq3el0y4z4jctmd9s29.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgq3el0y4z4jctmd9s29.png" alt="8.png" width="800" height="139"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  On-device inference was where the project got honest again
&lt;/h2&gt;

&lt;p&gt;Getting the model into a live app was one of the most satisfying parts of the project, but it was also where some of the remaining problems became impossible to ignore.&lt;/p&gt;

&lt;p&gt;The debugging app did not exist to look polished. It existed so I could run the loop end to end and see what actually held up. It loaded the model, extracted features on device, applied normalization, ran inference continuously, and used smoothing and thresholds to decide whether chewing was happening.&lt;/p&gt;

&lt;p&gt;That last part mattered more than I expected. A raw probability stream is not a product. It flickers, hesitates, and behaves like a model output. To make it feel usable, I had to think about state transitions, hysteresis, smoothing, and what kind of behavior a user would actually interpret as stable.&lt;/p&gt;

&lt;p&gt;The debugging app also exposed one of the clearest remaining gaps: the on-device feature extraction did not yet perfectly mirror the Python training pipeline. Some of the frequency-based features still needed fuller implementation on the Swift side. That is the kind of issue you can ignore for a while if you stay in notebooks and exports, but not if you run the system live.&lt;/p&gt;

&lt;p&gt;So in a strange way, the debugging app was valuable because it stopped me from lying to myself. The model had been trained. The model had been exported. But deployment is not real just because a CoreML file exists. It is real when the runtime path matches what the model expects closely enough that the behavior means something.&lt;/p&gt;

&lt;p&gt;I needed that reminder.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is working now, and what is still not there yet
&lt;/h2&gt;

&lt;p&gt;What is working is the core stack.&lt;/p&gt;

&lt;p&gt;I have a data collection workflow that is much more mature than the earliest versions. I have a clearer labeling path. I have a structured ML pipeline. I have a small model that can run on device. I have a debugging loop that lets me see the whole system behave in real time. That is real progress, and I trust it more because it came out of multiple failed or overcomplicated versions.&lt;/p&gt;

&lt;p&gt;What is still not there yet is the final product shape.&lt;/p&gt;

&lt;p&gt;The consumer-facing shell is still behind the infrastructure. The Swift-side runtime still needs full feature parity with the Python pipeline. The dataset still needs to get broader and more rigorous. The evaluation story needs to get stronger too, especially if I want to talk about the model as stable instead of just promising.&lt;/p&gt;

&lt;p&gt;So I do not think this project is finished, but I also do not think it is vague anymore. It is past the stage of being just an interesting idea. It has a real workflow, a real model path, and a real on-device loop. What it needs now is less invention and more tightening.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I think I actually learned from building this
&lt;/h2&gt;

&lt;p&gt;I learned that I tend to overbuild early, especially when a problem is interesting enough to support multiple plausible directions.&lt;/p&gt;

&lt;p&gt;I learned that native systems are often better than custom abstractions when the real goal is reliability.&lt;/p&gt;

&lt;p&gt;I learned that labeling and data quality are not side quests. They are the work.&lt;/p&gt;

&lt;p&gt;I learned that motion-data models can stay small and still be useful, which shifts the challenge toward signal understanding, feature design, data coverage, and runtime consistency.&lt;/p&gt;

&lt;p&gt;I learned a lot more about the motion patterns involved in jaw movement and eating than I knew when I started, and that changed the way I thought about the whole project. The sensor signal is not magic, but it is also not arbitrary. There is enough structure there to do something real with, if the surrounding system is disciplined enough.&lt;/p&gt;

&lt;p&gt;And I learned that building something like this is less about one app getting steadily better and more about a set of tools slowly learning how to work together: collector, labeler, transformer, trainer, exporter, and runtime debugger.&lt;/p&gt;

&lt;p&gt;That is the version of the project I trust now.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ljk2kb7ldd9lj2m8nw2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ljk2kb7ldd9lj2m8nw2.png" alt="12.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The next step is not to make it look more finished than it is. The next step is to make the current system more trustworthy.&lt;/p&gt;

&lt;p&gt;That means collecting more sessions, widening the dataset, tightening evaluation, finishing the bridge between Python and Swift, and only then giving the end-user app the kind of polish it actually deserves. I still want the final product layer, but I do not want to use design to hide uncertainty in the pipeline underneath it.&lt;/p&gt;

&lt;p&gt;So that is where ChewSense stands for me right now. It started as a curiosity about whether AirPods motion data could tell me anything useful about eating, and it turned into a much broader lesson in applied ML, data workflows, native tooling, and the cost of solving problems in the wrong order.&lt;/p&gt;

&lt;p&gt;I am still working on it. More is coming. But this is the first version of the project that feels like it has earned its next step.&lt;/p&gt;

&lt;p&gt;I am still refining ChewSense, so the repos are the clearest way to see what is real, what is evolving, and what comes next. If you have built something similar, want to compare notes, or just want to follow the project, reach out on &lt;a href="https://linkedin.com/in/zacharysturman" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, explore the work on &lt;a href="https://github.com/zsturman" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, or start with these repos: &lt;a href="https://github.com/ZSturman/ChewSense-Collection" rel="noopener noreferrer"&gt;ChewSense-Collection&lt;/a&gt;, &lt;a href="https://github.com/ZSturman/ChewSense---Data-Collection-and-Labeling" rel="noopener noreferrer"&gt;ChewSense—Data-Collection-and-Labeling&lt;/a&gt;, and &lt;a href="https://github.com/ZSturman/Chew-Sense" rel="noopener noreferrer"&gt;Chew-Sense&lt;/a&gt;. More context on my broader work is at &lt;a href="https://zachary-sturman.com/" rel="noopener noreferrer"&gt;zachary-sturman.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>airpods</category>
      <category>appliedml</category>
      <category>chewsense</category>
      <category>healthtech</category>
    </item>
    <item>
      <title>How I Use Notion to Ingest, Organize, and Track My Projects</title>
      <dc:creator>Zachary Sturman</dc:creator>
      <pubDate>Sun, 21 Jun 2026 16:16:54 +0000</pubDate>
      <link>https://dev.to/zacharysturman/how-i-use-notion-to-ingest-organize-and-track-my-projects-52a2</link>
      <guid>https://dev.to/zacharysturman/how-i-use-notion-to-ingest-organize-and-track-my-projects-52a2</guid>
      <description>&lt;p&gt;Notion is where almost every project I work on starts.&lt;/p&gt;

&lt;p&gt;Not just as a place to take notes, and not just as a task manager either. For me, it works more like a project intake system, a portfolio control center, and a structured source of truth for everything that happens once an idea starts turning into something real.&lt;/p&gt;

&lt;p&gt;Over time, I kept running into the same kinds of problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I would start building before setting up version control&lt;/li&gt;
&lt;li&gt;I would forget to write a clear one-line description for a project&lt;/li&gt;
&lt;li&gt;I would lose track of whether something was actually ready for my portfolio&lt;/li&gt;
&lt;li&gt;I would end up with media scattered across different places with no real structure&lt;/li&gt;
&lt;li&gt;I would leave project pages half-finished in ways that broke later automations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built a Notion workflow that makes those mistakes a little harder to make.&lt;/p&gt;

&lt;p&gt;This article walks through how I use that system when I create a new project, how I organize resources and assets, and how I use a Required Action field to guide the next step at each stage.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5gnmj6fyzo828jvy0iha.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5gnmj6fyzo828jvy0iha.png" alt="hero.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built my Notion project system this way
&lt;/h2&gt;

&lt;p&gt;I do not use Notion as a passive archive. I use it as an intake pipeline.&lt;/p&gt;

&lt;p&gt;When I add a new project, I want the page to do a few things right away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tell me what is missing&lt;/li&gt;
&lt;li&gt;keep the project compatible with my portfolio pipeline&lt;/li&gt;
&lt;li&gt;create structure for milestones, tasks, and work logs&lt;/li&gt;
&lt;li&gt;separate media and resources into their own databases&lt;/li&gt;
&lt;li&gt;make it easier for future automations to understand what state the project is in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part matters a lot.&lt;/p&gt;

&lt;p&gt;A project in pre-development should be handled differently from one that is actively being built. An archived project should not be described the same way as one that is still in progress. A private project should not accidentally show up on my public portfolio. So instead of treating project pages like loose notes, I try to make each one function more like a controlled system.&lt;/p&gt;

&lt;p&gt;The larger goal is consistency. I do not want to manually remember every rule for every project. I want the page itself to tell me what is missing, what matters most, and what should happen next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Every new project starts from a template
&lt;/h2&gt;

&lt;p&gt;When I want to ingest a new project, I click &lt;strong&gt;New&lt;/strong&gt; in my projects database and choose my &lt;strong&gt;Technology&lt;/strong&gt; template.&lt;/p&gt;

&lt;p&gt;I would share the template publicly, but I am still refining how I use Notion, and it depends on several linked databases and supporting tables. At this point, it makes more sense to describe the structure than to export something that only works halfway. If you want more detail on it, I can share more directly through my contact page.&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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F1%2520Create%2520new%2520project.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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F1%2520Create%2520new%2520project.mp4" alt="1 Create new project.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The template includes a few important controls right away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;Project Starter&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;Add Work Log Entry&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;Assets&lt;/strong&gt; toggle&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;Resources&lt;/strong&gt; toggle&lt;/li&gt;
&lt;li&gt;several linked visualizations and quick-access databases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those linked sections include things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;milestones&lt;/li&gt;
&lt;li&gt;tasks&lt;/li&gt;
&lt;li&gt;collections&lt;/li&gt;
&lt;li&gt;notes&lt;/li&gt;
&lt;li&gt;assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Collections piece is especially useful when a project includes multiple related sub-items that should stay grouped under the same umbrella.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first thing I look at is Required Action
&lt;/h2&gt;

&lt;p&gt;The center of the system is a property called &lt;strong&gt;Required Action&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This field tells me what needs to happen next and how serious the issue is. It works like a lightweight gatekeeper for the project page.&lt;/p&gt;

&lt;p&gt;The basic idea is simple. Instead of relying on memory, the page itself tells me what is still missing.&lt;/p&gt;

&lt;p&gt;When a new page is first created, the required action is usually something obvious like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔴🔴🔴 Add a title&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That sounds small, but it sets the tone for the rest of the system. The page is not considered ready until the page itself says it is.&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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F2%2520Add%2520title.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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F2%2520Add%2520title.mp4" alt="2 Add title.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As I fill things in, that field keeps updating. It changes based on what is missing, what matters most, and what is going to affect downstream systems.&lt;/p&gt;

&lt;p&gt;Behind the scenes, this is not just a static checklist. The page checks for missing or conflicting metadata, ranks issues by severity, and then surfaces only the single highest-priority next step. I pair that with a separate severity field so the logic can distinguish between critical setup problems, important structural issues, and smaller improvements.&lt;/p&gt;

&lt;p&gt;That matters because not every missing detail is equally important. A missing repo link is not the same kind of problem as a missing icon. A missing public flag for a showcase project matters more than a missing optional asset. By surfacing only the next most important fix, cleanup stays sequential instead of becoming a long list of unrelated problems.&lt;/p&gt;

&lt;p&gt;In practice, the system is checking for things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;title&lt;/li&gt;
&lt;li&gt;domain&lt;/li&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;one-liner&lt;/li&gt;
&lt;li&gt;summary&lt;/li&gt;
&lt;li&gt;started date&lt;/li&gt;
&lt;li&gt;category&lt;/li&gt;
&lt;li&gt;phase&lt;/li&gt;
&lt;li&gt;tags&lt;/li&gt;
&lt;li&gt;repo or an explicit no-repo exception&lt;/li&gt;
&lt;li&gt;core visual assets like thumbnail or banner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of those checks are conditional. Showcase projects have stricter requirements than ordinary ones, and technology projects can trigger extra checks, like requiring a repo unless I explicitly mark that one is not needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  After the title, I usually connect the repo
&lt;/h2&gt;

&lt;p&gt;Once I add the title, the next likely Required Action for a technology project is related to version control.&lt;/p&gt;

&lt;p&gt;That is intentional.&lt;/p&gt;

&lt;p&gt;There is a good chance a technology project should point to a repo, and I want to catch that early, because otherwise I might start doing real work before the project is version-controlled properly.&lt;/p&gt;

&lt;p&gt;In my setup, I do not just paste a GitHub URL into a single text property.&lt;/p&gt;

&lt;p&gt;Instead, I click &lt;strong&gt;Add Repo&lt;/strong&gt; inside the Resources section.&lt;/p&gt;

&lt;p&gt;That creates a dedicated resource entry using a pre-made GitHub template in a separate Resources database. Then I add the repo URL there.&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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F3%2520Add%2520repo.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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F3%2520Add%2520repo.mp4" alt="3 Add repo.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I do it this way because resources can be a lot of different things. Some are repos, some are links, some are install pages, some are references or files. Putting them in a dedicated database makes them easier to standardize and a lot more useful later.&lt;/p&gt;

&lt;p&gt;That structure is more involved than a simple URL field, but it gives me much more flexibility for automation, filtering, and display.&lt;/p&gt;

&lt;p&gt;If a project genuinely does not need a repo, I can explicitly mark that too. I prefer that over leaving it ambiguous, because it tells the system the requirement was considered rather than just forgotten.&lt;/p&gt;




&lt;h2&gt;
  
  
  Status is not just a label, it changes how the system behaves
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Status&lt;/strong&gt; field is one of the most important fields on the page.&lt;/p&gt;

&lt;p&gt;That is because status does more than describe where a project is. It tells other tools in my system how they should treat that project.&lt;/p&gt;

&lt;p&gt;An archived project should not sound the same in an article as a project that is actively being built. A pre-development concept should not trigger the same kinds of workflows as something that is already live. Status affects work logs, articles, and other automation steps throughout my pipeline.&lt;/p&gt;

&lt;p&gt;So when Required Action tells me to set the project status, that is not just a cosmetic suggestion. It is helping define the project’s operational state.&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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F4%2520Add%2520tags%2520and%2520summary.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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F4%2520Add%2520tags%2520and%2520summary.mp4" alt="4 Add tags and summary.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once status is set to something like &lt;strong&gt;Active&lt;/strong&gt;, the next required step may change again. For example, if the project is active and does not yet have a start date, the system can flag that.&lt;/p&gt;

&lt;p&gt;I intentionally treat the start date as a softer requirement than something like a missing title or a public mismatch, because in some cases that date can be derived automatically later if the project is active.&lt;/p&gt;

&lt;p&gt;That is why the severity changes. Red means something critical is missing. Yellow means the project is usable, but still incomplete.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-liner exists for my portfolio, not just for me
&lt;/h2&gt;

&lt;p&gt;One of the next important fields is the &lt;strong&gt;one-liner&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is specifically for my portfolio. I want every public project to have a clear, concise description that explains what it is in a single sentence.&lt;/p&gt;

&lt;p&gt;That sentence ends up being useful in several places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;portfolio cards&lt;/li&gt;
&lt;li&gt;previews&lt;/li&gt;
&lt;li&gt;summaries&lt;/li&gt;
&lt;li&gt;skimmable project lists&lt;/li&gt;
&lt;li&gt;internal reference when I come back to an older project later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So once the status is set, Required Action may prompt me to add that one-line description next.&lt;/p&gt;

&lt;p&gt;This helps me avoid a common problem, which is building the project first and only trying to explain it later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary is for clarity, not just metadata completeness
&lt;/h2&gt;

&lt;p&gt;The project summary is a little different from the one-liner.&lt;/p&gt;

&lt;p&gt;The one-liner is short and external-facing. The summary is broader and gives the project page more shape.&lt;/p&gt;

&lt;p&gt;I use it partly to make the project detail page cleaner, and partly to help my future self stay oriented. If I come back to a project after some time away, I do not want to reconstruct its purpose from scattered tasks and notes. I want the page to tell me what it is, why it exists, and what kind of thing it is trying to become.&lt;/p&gt;

&lt;p&gt;This is one of those fields that pays off later more than it seems like it will in the moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Media is structured as assets, not attachments
&lt;/h2&gt;

&lt;p&gt;The next major piece is media.&lt;/p&gt;

&lt;p&gt;Rather than attaching images loosely to a project page, I use an &lt;strong&gt;Assets&lt;/strong&gt; database. From the project page, I can click buttons like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thumbnail&lt;/li&gt;
&lt;li&gt;Banner&lt;/li&gt;
&lt;li&gt;Icon&lt;/li&gt;
&lt;li&gt;Poster&lt;/li&gt;
&lt;li&gt;Hero&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those creates a new row in the Assets database using the appropriate template and links it back to the current project.&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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F5%2520Add%2520thumbnail.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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F5%2520Add%2520thumbnail.mp4" alt="5 Add thumbnail.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, clicking &lt;strong&gt;+ Thumbnail&lt;/strong&gt; creates a thumbnail asset that is already connected to the project.&lt;/p&gt;

&lt;p&gt;That setup is definitely more elaborate than what a simple project tracker needs, but I built it this way because of how my portfolio automations work later. I want assets to be typed, reusable, and individually addressable instead of being mixed together in one generic media field.&lt;/p&gt;

&lt;p&gt;If there is no banner image, my portfolio can automatically fall back to the thumbnail. Even so, I still prefer to create multiple asset variants because different placements on the site have different visual requirements.&lt;/p&gt;

&lt;p&gt;That makes the Notion structure a little heavier up front, but it keeps the downstream system much cleaner.&lt;/p&gt;




&lt;h2&gt;
  
  
  Not every missing item should block the project
&lt;/h2&gt;

&lt;p&gt;One thing I like about this setup is that not all missing details are treated the same way.&lt;/p&gt;

&lt;p&gt;For example, a project may have no more top-level required actions, but still show something like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🟡 Add an icon&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a separate media-related signal rather than a full project blocker.&lt;/p&gt;

&lt;p&gt;I did that on purpose.&lt;/p&gt;

&lt;p&gt;A lot of the time, I want extra media to round out the portfolio more fully, but I do not want the top-level Required Action field to block other automations if the project is otherwise ready to go.&lt;/p&gt;

&lt;p&gt;So I separated &lt;strong&gt;must be complete for the project to function&lt;/strong&gt; from &lt;strong&gt;would improve presentation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That distinction matters more than it may seem. It lets the system push quality upward without turning every optional improvement into a failure state. The project can be operationally complete while still carrying smaller asset-related nudges.&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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F10%2520Add%2520icon.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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F10%2520Add%2520icon.mp4" alt="10 Add icon.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That keeps the workflow practical. It still encourages polish, but it does not treat polish as the same thing as readiness.&lt;/p&gt;




&lt;h2&gt;
  
  
  Public, showcase, and featured are where portfolio logic starts to matter
&lt;/h2&gt;

&lt;p&gt;This is the point where the page stops being only a personal project tracker and starts acting more like a portfolio control center.&lt;/p&gt;

&lt;p&gt;There is a &lt;strong&gt;Public&lt;/strong&gt; toggle. If it is turned off, my automation tools ignore the project.&lt;/p&gt;

&lt;p&gt;That means it will not feed into certain public-facing systems, and it also will not be acknowledged by parts of my broader setup that expect the project to be publicly visible.&lt;/p&gt;

&lt;p&gt;There is also an &lt;strong&gt;In Showcase&lt;/strong&gt; toggle. If that is turned on, the project is intended to appear in my portfolio.&lt;/p&gt;

&lt;p&gt;But showcase projects are held to a stricter standard than ordinary ones. If a project is marked &lt;strong&gt;In Showcase&lt;/strong&gt; while &lt;strong&gt;Public&lt;/strong&gt; is still off, the Required Action field catches that mismatch and flags it clearly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔴🔴🔴 Project must be public to appear in showcase&lt;/strong&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%2Fraw.githubusercontent.com%2FZSturman%2FArticles%2Fmain%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F7%2520Must%2520be%2520public.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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F7%2520Must%2520be%2520public.mp4" alt="7 Must be public.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is one of my favorite examples of why I built the system this way. Instead of depending on me to remember a rule, the page enforces the logic for me.&lt;/p&gt;

&lt;p&gt;If I later mark a project as &lt;strong&gt;Featured&lt;/strong&gt;, that affects the carousel at the top of my portfolio page. If I do not provide a featured order, the system can still place it automatically, but it is still better to set the order intentionally.&lt;/p&gt;

&lt;p&gt;So Required Action may shift again to something like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🟠 Set featured order&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once that order is set, the project can return to a clean state.&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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F8%2520Need%2520featured%2520order.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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F8%2520Need%2520featured%2520order.mp4" alt="8 Need featured order.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is where Notion becomes much more than a note-taking space for me. It becomes a way to define how a project should behave across the rest of my portfolio system.&lt;/p&gt;

&lt;p&gt;I go into the backend side of that more in another write-up:&lt;/p&gt;

&lt;p&gt;&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;That article covers the broader workflow, including how I connect Notion with the rest of my automation pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Category, phase, and tags help with structure, search, and presentation
&lt;/h2&gt;

&lt;p&gt;Once the core project is set up, I usually add the supporting metadata that makes the project easier to group and find later.&lt;/p&gt;

&lt;p&gt;That includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;category&lt;/li&gt;
&lt;li&gt;phase&lt;/li&gt;
&lt;li&gt;tags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These fields help in a few ways.&lt;/p&gt;

&lt;p&gt;They improve how projects are grouped and filtered internally. They help with portfolio organization. They also support things like SEO and site-wide search in the systems connected to my portfolio.&lt;/p&gt;

&lt;p&gt;So while these fields are not always the first things I fill out, they become important pretty quickly once the project has passed the earliest intake steps.&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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F6%2520Add%2520category%2520phase%2520and%2520tags.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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F6%2520Add%2520category%2520phase%2520and%2520tags.mp4" alt="6 Add category phase and tags.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is another case where I am trying to make the data do real work instead of just filling out metadata for its own sake.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Project Starter button is where setup becomes momentum
&lt;/h2&gt;

&lt;p&gt;Once the page itself is in a good state, I want to move from describing the project to actually managing it.&lt;/p&gt;

&lt;p&gt;That is what the &lt;strong&gt;Project Starter&lt;/strong&gt; button is for.&lt;/p&gt;

&lt;p&gt;This article is not mainly about my task management system, but this button is one of the most useful parts of the template because it creates momentum right away.&lt;/p&gt;

&lt;p&gt;When I click it, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adds a default milestone that is usually relevant for technology projects&lt;/li&gt;
&lt;li&gt;adds the tasks connected to that milestone&lt;/li&gt;
&lt;li&gt;creates a new work log entry showing the project has started&lt;/li&gt;
&lt;/ul&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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F11%2520Project%2520starter.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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F11%2520Project%2520starter.mp4" alt="11 Project starter.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That means I do not have to manually build the same starting structure every time. The project page starts as intake, then moves directly into execution.&lt;/p&gt;

&lt;p&gt;If you want a deeper breakdown of how I handle task management and how I connect that to Linear, that belongs in a separate article: &lt;a href="https://zachary-sturman.com/articles/automating-linear-from-notion" rel="noopener noreferrer"&gt;https://zachary-sturman.com/articles/automating-linear-from-notion&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I keep resources and assets in their own databases
&lt;/h2&gt;

&lt;p&gt;This is probably the part of the setup that feels most extra from the outside, but for me it solves a real problem.&lt;/p&gt;

&lt;p&gt;A project contains a lot of different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repos&lt;/li&gt;
&lt;li&gt;files&lt;/li&gt;
&lt;li&gt;install links&lt;/li&gt;
&lt;li&gt;visit links&lt;/li&gt;
&lt;li&gt;thumbnails&lt;/li&gt;
&lt;li&gt;banners&lt;/li&gt;
&lt;li&gt;icons&lt;/li&gt;
&lt;li&gt;notes&lt;/li&gt;
&lt;li&gt;collections&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all of that lives directly on the main page as miscellaneous properties, the page gets harder to maintain, and automation gets much more fragile.&lt;/p&gt;

&lt;p&gt;By splitting out &lt;strong&gt;Resources&lt;/strong&gt; and &lt;strong&gt;Assets&lt;/strong&gt; into dedicated databases, I get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more consistent templates&lt;/li&gt;
&lt;li&gt;better linking between entities&lt;/li&gt;
&lt;li&gt;more scalable organization&lt;/li&gt;
&lt;li&gt;cleaner project pages&lt;/li&gt;
&lt;li&gt;stronger compatibility with later automations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So yes, the setup is more involved than the simplest possible Notion tracker. But it is also much more reusable, and it helps me treat projects as structured systems instead of messy pages.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this system is really doing for me
&lt;/h2&gt;

&lt;p&gt;On the surface, this workflow looks like a way to make cleaner Notion pages.&lt;/p&gt;

&lt;p&gt;But that is not really the point.&lt;/p&gt;

&lt;p&gt;The point is that it reduces friction at the exact moments where I usually make mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;forgetting to create the repo first&lt;/li&gt;
&lt;li&gt;forgetting to define the project clearly&lt;/li&gt;
&lt;li&gt;forgetting to mark whether it is public&lt;/li&gt;
&lt;li&gt;forgetting the media needed for the portfolio&lt;/li&gt;
&lt;li&gt;forgetting the fields that later systems depend on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of trying to remember every rule, I built the rules into the page.&lt;/p&gt;

&lt;p&gt;That is what makes this useful. The system tells me what to do next. It nudges me toward consistency. It lets me move faster without introducing as much hidden chaos.&lt;/p&gt;

&lt;p&gt;And because the structure connects to the rest of my portfolio tooling, the project page is not just documentation. It is part of the pipeline.&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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F13%2520Final%2520product.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%2Fhow-i-use-notion-to-track-my-projects%2Fimages%2F13%2520Final%2520product.mp4" alt="13 Final product.mov" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;This is most of the Notion setup I use when creating a new project.&lt;/p&gt;

&lt;p&gt;It starts as a project intake flow, but it also becomes the control layer for how that project is tracked, described, displayed, and connected to the rest of my system.&lt;/p&gt;

&lt;p&gt;There is still more to the broader workflow than what I covered here. In particular, the rest of the project lifecycle connects into my portfolio sync process, including how I handle automation, deployment, and the systems that turn these project records into something my site can actually use.&lt;/p&gt;

&lt;p&gt;For that side of it, I will point to a separate breakdown:&lt;/p&gt;

&lt;p&gt;&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;And if you want to understand the task side more deeply: &lt;a href="https://zachary-sturman.com/articles/automating-linear-from-notion" rel="noopener noreferrer"&gt;https://zachary-sturman.com/articles/automating-linear-from-notion&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to know how I built this system, want help setting up something similar for yourself, or have ideas for ways I could optimize it further, feel free to reach out through my contact page.&lt;/p&gt;

&lt;p&gt;You can also check out my portfolio here: &lt;a href="https://zachary-sturman.com/" rel="noopener noreferrer"&gt;zachary-sturman.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>apiintegration</category>
      <category>machinelearning</category>
      <category>notion</category>
    </item>
    <item>
      <title>Building a Desktop App Around Google Sheets</title>
      <dc:creator>Zachary Sturman</dc:creator>
      <pubDate>Sun, 07 Jun 2026 20:58:03 +0000</pubDate>
      <link>https://dev.to/zacharysturman/building-a-desktop-app-around-google-sheets-4keg</link>
      <guid>https://dev.to/zacharysturman/building-a-desktop-app-around-google-sheets-4keg</guid>
      <description>&lt;p&gt;When I moved to Washington after getting back from Australia, a client told me they were emailing Excel files back and forth to manage production schedules. Not a polished internal system, but just spreadsheets moving around between people because that was what they had.&lt;/p&gt;

&lt;p&gt;That was the moment this project really started for me.&lt;/p&gt;

&lt;p&gt;I didn’t begin with a big product thesis. I just thought there had to be a better way to handle something this central to how work moved through a business. The more I thought about it, the more obvious it felt that the scheduling problem was not really about one spreadsheet. It was about a fragile process built on top of tools that were doing more work than they were designed for.&lt;/p&gt;

&lt;p&gt;So I started building.&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%2F4liq9zjqt64zvo0spcp7.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%2F4liq9zjqt64zvo0spcp7.png" alt="V1 desktop concept mockup.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  V1 Started As A Desktop App Because That Was How I Was Thinking About The Problem
&lt;/h3&gt;

&lt;p&gt;The first version was a desktop app, and in hindsight that choice makes perfect sense to me even if I would not make the same choice the same way now.&lt;/p&gt;

&lt;p&gt;The people I was thinking about were already living in spreadsheet-shaped workflows. They were used to desktop tools, used to files, used to opening something local and working directly with data. At the same time, Tauri v2 had just come out, and I was excited to learn it. That mattered more than I admitted to myself at the time.&lt;/p&gt;

&lt;p&gt;I was not only solving a problem. I was also following the momentum of a technical curiosity that happened to line up with the kind of workflow I was looking at.&lt;/p&gt;

&lt;p&gt;That combination pushed me toward a desktop app that could work closely with spreadsheet data, local credentials, and scheduling logic. It felt practical. It felt close to the world the users were already in. It also let me learn a framework I had wanted to get my hands on.&lt;/p&gt;

&lt;p&gt;I still think that instinct was understandable. What changed later was not that the idea was foolish. What changed was my understanding of where the real friction lived.&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%2Fxcmz3kvm85xkasch1kmh.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%2Fxcmz3kvm85xkasch1kmh.png" alt="Prototype-to-product evolution graphic.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Desktop App Taught Me More Than I Expected
&lt;/h3&gt;

&lt;p&gt;I learned a lot from building the desktop version, especially because it forced me to deal with things I could have ignored for longer in a pure web build.&lt;/p&gt;

&lt;p&gt;The hardest part was security and access.&lt;/p&gt;

&lt;p&gt;That sounds broad, but it ended up touching everything. Once you are dealing with local files, credentials, operating system behavior, packaging, and permissions, a lot of the work stops being about your app’s happy path and starts being about the edges around it. That was even more noticeable because I was building on a Mac while thinking about a Windows environment.&lt;/p&gt;

&lt;p&gt;There were a lot of unknowns. More importantly, there were a lot of unknown unknowns.&lt;/p&gt;

&lt;p&gt;Some of that was platform-specific. Some of it was just the cost of building close to the machine. But either way, it changed how I thought about product scope. I was not only building scheduling logic or data flows. I was also taking on the realities of desktop distribution, local access, and cross-platform behavior.&lt;/p&gt;

&lt;p&gt;That was valuable. I do not see the desktop version as wasted effort.&lt;/p&gt;

&lt;p&gt;It taught me how much invisible complexity lives in the layer between “this app works on my machine” and “this is easy enough for someone else to adopt without help.” It also made me much more aware of how quickly technical enthusiasm can shape product direction before the product itself has earned that level of commitment.&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%2Fmfg7zdz38um548swfwid.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%2Fmfg7zdz38um548swfwid.png" alt="Adoption friction comparison.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Sheets Was A Practical Choice, Not A Romantic One
&lt;/h3&gt;

&lt;p&gt;One of the clearest decisions in this project was Google Sheets.&lt;/p&gt;

&lt;p&gt;I chose it because it was free.&lt;/p&gt;

&lt;p&gt;That sounds almost too simple, but I think simple reasons are often the honest ones. I was working around a spreadsheet-driven workflow already, and I did not want the first version of the solution to introduce more cost than necessary. Starting from a familiar, cheap foundation made it possible to focus on the workflow itself instead of treating infrastructure as the first problem.&lt;/p&gt;

&lt;p&gt;That decision also shaped the product in useful ways. It kept the system close to the data format people already understood, and it gave me a practical base for configuration, scheduling data, and later web-based setup flows.&lt;/p&gt;

&lt;p&gt;At the same time, it created its own constraints. When your product grows around spreadsheets, you eventually have to think hard about validation, structure drift, setup quality, and how much flexibility you actually want to expose. That became more obvious in the later web version, where configuration and onboarding started to matter much more.&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%2F4zst42isqxtkag0l7oyz.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%2F4zst42isqxtkag0l7oyz.png" alt="Google Sheets as infrastructure visual.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bigger Problem Turned Out To Be Distribution
&lt;/h3&gt;

&lt;p&gt;The main reason I moved toward the web was not that I stopped caring about desktop, or that the desktop version taught me nothing useful.&lt;/p&gt;

&lt;p&gt;It was that having someone download an app is a lot to ask.&lt;/p&gt;

&lt;p&gt;That sounds obvious when I write it now, but it was not obvious enough to me at the start. I was focused on the workflow problem. I was focused on the technical challenge. I was focused on what the tool could do once someone had it.&lt;/p&gt;

&lt;p&gt;I was not focused enough on the moment before all of that, the point where someone decides whether they are even willing to try it.&lt;/p&gt;

&lt;p&gt;That changed the project for me.&lt;/p&gt;

&lt;p&gt;The install step was not a neutral detail. It was part of the product. If the barrier to entry is too high, the architecture underneath it almost does not matter. A desktop app can be the right answer in a lot of cases, but in this case it started to feel like I was asking users to make too big a commitment too early.&lt;/p&gt;

&lt;p&gt;That realization pushed me toward a web version.&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%2Fzu0preyfl74yofhmsmbs.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%2Fzu0preyfl74yofhmsmbs.png" alt="Desktop complexity diagram.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  V2 Changed More Than The Delivery Model
&lt;/h3&gt;

&lt;p&gt;The web version is not just the same idea wrapped in a browser.&lt;/p&gt;

&lt;p&gt;What changed in V2 is that I started thinking more seriously about setup, flexibility, and adoption. Instead of centering the product around one installed app, I started building toward a system that could handle onboarding, credentials, configuration, templates, reusable views, and different ways of shaping spreadsheet data into something more usable.&lt;/p&gt;

&lt;p&gt;That shift changed the structure of the project.&lt;/p&gt;

&lt;p&gt;The newer direction is more modular. It is less about one hard-coded workflow and more about making the path into the product easier to manage. There is more attention on setup flows, business-level configuration, template selection, widget-like views, and dashboard-style usage. The scheduling side still matters, especially the Gantt-style view of work moving through a process, but it sits inside a broader product shape now.&lt;/p&gt;

&lt;p&gt;That feels more honest to the actual problem.&lt;/p&gt;

&lt;p&gt;If the original pain point was people passing spreadsheet files around to keep operations moving, then the solution cannot only be “replace the spreadsheet.” It also has to answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How does someone connect their data safely?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How much setup is required?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do they understand what is missing or mismatched?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do they get from raw spreadsheet structure to something usable without a lot of manual interpretation?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The web version moves closer to those questions.&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%2Fr6uk2cb6hqjxe0gdcvfs.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%2Fr6uk2cb6hqjxe0gdcvfs.png" alt="V2 modular system architecture.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Would Handle Differently Now
&lt;/h3&gt;

&lt;p&gt;The biggest thing I would change is not a framework choice.&lt;/p&gt;

&lt;p&gt;I would take more time to run through prototypes and test with users earlier.&lt;/p&gt;

&lt;p&gt;That does not mean I regret building the desktop app. I learned a lot from it, and some of those lessons were only available because I built something real enough to push against. But I do think I committed too early to one delivery model before pressure-testing the path people would take into the product.&lt;/p&gt;

&lt;p&gt;I think that is easy to do when the problem is vivid and the build is exciting.&lt;/p&gt;

&lt;p&gt;You see the broken workflow. You know you can improve it. A framework clicks with the kind of system you want to build. You start connecting those dots and it feels like momentum. Sometimes it is momentum. Sometimes it is just speed in the wrong direction.&lt;/p&gt;

&lt;p&gt;What I trust more now is smaller proof before bigger commitment.&lt;/p&gt;

&lt;p&gt;If I were starting this again, I would still pay close attention to the real spreadsheet workflow. I would still keep cost in mind. I would still care about scheduling and visibility. But I would put much more energy into lightweight prototypes, user feedback, and adoption friction before I let the implementation get too far ahead of the product shape.&lt;/p&gt;

&lt;p&gt;That is probably the most useful thing this project taught me.&lt;/p&gt;

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

&lt;p&gt;I still like this project because it taught me in public, even when the audience was mostly me.&lt;/p&gt;

&lt;p&gt;V1 taught me how much complexity lives inside desktop software, especially when security, access, and cross-platform behavior start to matter. V2 is teaching me something different, which is that the best technical solution still has to meet people where they are, and sometimes that means reducing commitment before adding capability.&lt;/p&gt;

&lt;p&gt;I do not see the move from desktop to web as a clean correction. It feels more like a better question replacing an earlier one.&lt;/p&gt;

&lt;p&gt;The first question was, “How do I build a better tool for this workflow?”&lt;/p&gt;

&lt;p&gt;The better question turned out to be, “What would make this easier for someone to actually adopt?”&lt;/p&gt;

&lt;p&gt;That is the version of the problem I care more about now.&lt;/p&gt;

&lt;p&gt;If you have built internal tools around spreadsheet-heavy workflows, or you have had to rethink a project after building too much too early, I would be glad to compare notes. You can find me at &lt;a href="https://zachary-sturman.com/" rel="noopener noreferrer"&gt;zachary-sturman.com&lt;/a&gt;, &lt;a href="https://github.com/zsturman" rel="noopener noreferrer"&gt;github.com/zsturman&lt;/a&gt;, &lt;a href="https://linkedin.com/in/zacharysturman" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, or by email at &lt;a href="mailto:Zasturman@gmail.com"&gt;Zasturman@gmail.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>desktopapp</category>
      <category>googlesheets</category>
      <category>manufacturing</category>
      <category>saas</category>
    </item>
    <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&amp;nbsp;&lt;em&gt;“How’s My Eating?”&lt;/em&gt;&amp;nbsp;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>
