<?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: Danil Galeev</title>
    <description>The latest articles on DEV Community by Danil Galeev (@danilgaleev).</description>
    <link>https://dev.to/danilgaleev</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%2F4082182%2F23591c06-ef30-4c83-8711-aa1f37797bb6.png</url>
      <title>DEV Community: Danil Galeev</title>
      <link>https://dev.to/danilgaleev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danilgaleev"/>
    <language>en</language>
    <item>
      <title>A weekly reporting workflow that stops when its sources are incomplete</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Wed, 16 Sep 2026 13:18:03 +0000</pubDate>
      <link>https://dev.to/danilgaleev/a-weekly-reporting-workflow-that-stops-when-its-sources-are-incomplete-5abl</link>
      <guid>https://dev.to/danilgaleev/a-weekly-reporting-workflow-that-stops-when-its-sources-are-incomplete-5abl</guid>
      <description>&lt;p&gt;A weekly report can contain plausible numbers and still be unsuitable for a decision. A missing support export or an old CRM snapshot changes what the report can honestly say.&lt;/p&gt;

&lt;p&gt;I built a &lt;a href="https://madeby.expert/demo.html" rel="noopener noreferrer"&gt;small reporting demo for MadeBy.Expert&lt;/a&gt; to make those conditions visible. It uses synthetic billing, CRM and support snapshots, deterministic calculations and a template narrative. There are no live integrations or language model calls in this version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the numbers before automating the report
&lt;/h2&gt;

&lt;p&gt;The sample distinguishes monthly recurring revenue from revenue collected during the week. Open pipeline is the unweighted value of open opportunities, not a revenue forecast. Support metrics distinguish unresolved tickets at the snapshot from tickets resolved within the reporting period.&lt;/p&gt;

&lt;p&gt;Each metric includes a source reference and a definition. Those details give the reviewer something concrete to check against the input.&lt;/p&gt;

&lt;p&gt;For a client workflow, I would agree on these definitions before connecting the source systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  A missing source is a report state
&lt;/h2&gt;

&lt;p&gt;The browser demo lets you switch between complete sources, missing support data and stale CRM data. The report keeps the metrics it can calculate, omits the unavailable ones and records a blocker. It does not substitute zero for a missing value.&lt;/p&gt;

&lt;p&gt;The final state comes from that blocker list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;blockers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this example, all three sources are required for approval. A snapshot is stale if it was captured before the reporting period ended or more than 24 hours before the report's &lt;code&gt;asOf&lt;/code&gt; time. A future timestamp is rejected as invalid input.&lt;/p&gt;

&lt;p&gt;Those are choices for this workflow. A daily operations report might need a tighter freshness limit; another report might be useful with an explicitly optional source. The thresholds need agreement with the person using the result.&lt;/p&gt;

&lt;p&gt;Here is the actual test covering all three sources and both failure modes. It uses Node's built-in test runner and strict assertions. &lt;code&gt;fixture()&lt;/code&gt; loads the synthetic input afresh for each case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing/stale sources are unavailable and block approval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;billing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;support&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;capturedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-09-12T08:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;approveReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Demo reviewer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sr"&gt;/Blocked/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last assertion matters: reporting the problem in the output is insufficient if the approval function still accepts it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approval applies to one version of the report
&lt;/h2&gt;

&lt;p&gt;The implementation computes a SHA-256 digest of the report content, which includes a digest of the input. A reviewer supplies the report digest when approving a local export. The approval function recomputes it and checks that the report is still a draft without blockers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;approveReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Approval digest does not match the current report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Blocked reports cannot be approved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;reviewer&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\r\n]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A named reviewer is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;approval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;hash&lt;/code&gt; is SHA-256 over &lt;code&gt;JSON.stringify(value)&lt;/code&gt; and &lt;code&gt;fail&lt;/code&gt; throws an error. This is an excerpt from the local prototype, not a standalone approval service.&lt;/p&gt;

&lt;p&gt;Rebuilding the report after changing the input produces a different digest, so the previous digest cannot approve the new report. The tests also alter a calculated metric directly and check that approval rejects the modified content.&lt;/p&gt;

&lt;p&gt;There are limits to this approach. The digest is a consistency check, not a signature or proof of who reviewed the report. The reviewer name is supplied as text; there is no authentication or authorization layer. The JSON serialization is also specific to this implementation, not a canonical format for independent producers.&lt;/p&gt;

&lt;p&gt;A deployed approval service would need to establish the reviewer's identity and permissions, store the review event, and bind the delivery action to the approved version. Those parts are not implemented here.&lt;/p&gt;

&lt;p&gt;The browser page shows the data conditions and report. The local runnable example handles approval and export; neither sends a report automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI could fit later
&lt;/h2&gt;

&lt;p&gt;A language model might help draft commentary from validated facts. That is a possible extension, not an implemented feature of this demo. I would keep metric calculation, source checks and the approval boundary explicit regardless of how the narrative is produced.&lt;/p&gt;

&lt;p&gt;The current prototype makes no claim about client time savings, live connector reliability or model quality. Those need measurement against an actual workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://madeby.expert/demo.html" rel="noopener noreferrer"&gt;Try the demo&lt;/a&gt;, including the missing-source and stale-source cases.&lt;/p&gt;

&lt;p&gt;If your team assembles a similar report manually, &lt;a href="https://madeby.expert/#contact" rel="noopener noreferrer"&gt;describe the workflow&lt;/a&gt;: which tools supply the data, how often the report is needed, and who checks it. We can discuss whether a small paid pilot with clear success criteria fits.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>architecture</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>Typed Contracts Between AI Agents: The Interface That Actually Breaks</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Tue, 15 Sep 2026 05:44:04 +0000</pubDate>
      <link>https://dev.to/danilgaleev/typed-contracts-between-ai-agents-the-interface-that-actually-breaks-547j</link>
      <guid>https://dev.to/danilgaleev/typed-contracts-between-ai-agents-the-interface-that-actually-breaks-547j</guid>
      <description>&lt;p&gt;Most multi-agent failures I've seen don't happen &lt;em&gt;inside&lt;/em&gt; an agent. They happen &lt;strong&gt;between&lt;/strong&gt; two of them.&lt;/p&gt;

&lt;p&gt;When the handoff between two agents is treated as loosely structured JSON or implicit shared state, the interface is underspecified — and underspecified interfaces fail quietly.&lt;/p&gt;

&lt;p&gt;A production handoff should be treated like any other API contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an explicit input schema;&lt;/li&gt;
&lt;li&gt;an explicit output schema;&lt;/li&gt;
&lt;li&gt;required and optional fields;&lt;/li&gt;
&lt;li&gt;semantic constraints, not only valid JSON;&lt;/li&gt;
&lt;li&gt;a clear rejection and fallback path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A concrete example
&lt;/h2&gt;

&lt;p&gt;A research agent should not hand an unstructured object to a scoring agent. It should emit something the scorer can actually validate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResearchResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;subject_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EvidenceItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;source_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scorer runs only after that result passes validation. That gate is the important part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probabilistic output, consequential action
&lt;/h2&gt;

&lt;p&gt;An LLM output is probabilistic. A database write, an external message, or a business decision is an action. Those two steps should not be directly coupled.&lt;/p&gt;

&lt;p&gt;Between them, put a deterministic gate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse the model output.&lt;/li&gt;
&lt;li&gt;Validate its schema and business rules.&lt;/li&gt;
&lt;li&gt;Reject, retry, route to review, or stop the branch when validation fails.&lt;/li&gt;
&lt;li&gt;Execute the action only on an accepted contract.&lt;/li&gt;
&lt;li&gt;Record the handoff version and validation result.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why it changes failure behavior
&lt;/h2&gt;

&lt;p&gt;Without a contract, a missing field becomes a null value, the null becomes a misleading score, and the score becomes a bad CRM update. The symptom shows up several steps away from the cause, and by then the trail is cold.&lt;/p&gt;

&lt;p&gt;With contracts, the same failure is localized: &lt;code&gt;ResearchAgent -&amp;gt; ScoringAgent&lt;/code&gt;, field &lt;code&gt;source_count&lt;/code&gt;, validation rule failed.&lt;/p&gt;

&lt;p&gt;That is more than defensive programming. It is architecture for software systems whose internal steps are non-deterministic.&lt;/p&gt;

&lt;p&gt;It gives staff engineers independently testable boundaries, helps principal engineers reduce coupling between teams and agents, and gives architects and CTOs a clearer way to reason about compatibility, ownership, and recovery.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Every probabilistic step that feeds a consequential action needs an explicit validation gate. Typed contracts are one strong way to define that boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model can be creative inside the boundary. The boundary should not be.&lt;/p&gt;

&lt;p&gt;Where have you found the most valuable contract in an agent workflow — between agents, between an agent and a tool, or before the final action?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building AI Agents That Actually Work: What Nobody Tells You</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Fri, 04 Sep 2026 09:11:40 +0000</pubDate>
      <link>https://dev.to/danilgaleev/building-ai-agents-that-actually-work-what-nobody-tells-you-2f88</link>
      <guid>https://dev.to/danilgaleev/building-ai-agents-that-actually-work-what-nobody-tells-you-2f88</guid>
      <description>&lt;p&gt;I've been building AI agents for a while now. Not chatbots. Not RAG demos. Real agents that take actions, make decisions, and run autonomously.&lt;/p&gt;

&lt;p&gt;Here's what nobody tells you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between demo and production
&lt;/h2&gt;

&lt;p&gt;Every AI agent framework shows you a 5-line demo that works perfectly. Then you deploy it, and it fails in ways you didn't imagine.&lt;/p&gt;

&lt;p&gt;The reason is simple: a demo is a happy path. Production is a graph of failure states.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Observability is not optional
&lt;/h2&gt;

&lt;p&gt;Your agent is only as good as your ability to see what it's doing.&lt;/p&gt;

&lt;p&gt;When an agent makes a wrong decision, you need to know exactly why. Was it a bad prompt? A hallucinated tool call? Missing context from a previous step?&lt;/p&gt;

&lt;p&gt;Log every thought. Audit every action. If you can't replay an agent's decision process, you can't trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Tools are the bottleneck, not the LLM
&lt;/h2&gt;

&lt;p&gt;Most people think the LLM is the hard part. It's not. The hard part is the tools.&lt;/p&gt;

&lt;p&gt;Your agent needs to call APIs, read databases, write files, send emails. Each of those is a failure point. Network timeout. Auth expired. Schema changed. Rate limited.&lt;/p&gt;

&lt;p&gt;Build your tool layer like you build a distributed system. Retries. Circuit breakers. Timeouts. Graceful degradation.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The context window is a trap
&lt;/h2&gt;

&lt;p&gt;Long-running agents accumulate context. The more they do, the more context they carry. Eventually, the context window fills with noise, and the agent starts making bad decisions.&lt;/p&gt;

&lt;p&gt;Strategies that actually work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summarization&lt;/strong&gt; — compress old context into summaries, don't carry raw history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured memory&lt;/strong&gt; — separate short-term (current task) from long-term (learned patterns)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting&lt;/strong&gt; — actively prune irrelevant context. If it didn't matter in the last 10 steps, it probably won't matter now&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Determinism is underrated
&lt;/h2&gt;

&lt;p&gt;Everyone wants creative agents. What you actually want is predictable agents.&lt;/p&gt;

&lt;p&gt;A creative agent that hallucinates a solution is useless. A predictable agent that follows a known pattern is valuable.&lt;/p&gt;

&lt;p&gt;Design for determinism first. Add creativity as a controlled parameter.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The orchestration layer matters more than the model
&lt;/h2&gt;

&lt;p&gt;You can swap GPT-4 for Claude or Gemini and your agent still works — if your orchestration is solid.&lt;/p&gt;

&lt;p&gt;Good orchestration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear state machine&lt;/li&gt;
&lt;li&gt;Explicit error handling&lt;/li&gt;
&lt;li&gt;Human-in-the-loop for critical decisions&lt;/li&gt;
&lt;li&gt;Audit trail for every action&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The uncomfortable truth
&lt;/h2&gt;

&lt;p&gt;Building AI agents that actually work is not an AI problem. It's a software engineering problem.&lt;/p&gt;

&lt;p&gt;The LLM is the easiest part. Everything around it — tools, observability, state management, error handling, orchestration — that's where the real work is.&lt;/p&gt;

&lt;p&gt;And that's also where the real value is.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Your Agent Isn't Losing Memory. It's Rotting.</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Fri, 28 Aug 2026 18:33:00 +0000</pubDate>
      <link>https://dev.to/danilgaleev/your-agent-isnt-losing-memory-its-rotting-2edd</link>
      <guid>https://dev.to/danilgaleev/your-agent-isnt-losing-memory-its-rotting-2edd</guid>
      <description>&lt;p&gt;Agents don't forget because the model is bad. They forget because we built it into them. Six hours and forty messages in, what an agent needs is gone — or still there but wrong, drowned in noise.&lt;/p&gt;

&lt;p&gt;That's context rot.&lt;/p&gt;

&lt;p&gt;I've spent the last month inside a team of always-on AI agents that run for weeks. Different jobs, one codebase, one shared vault of lessons. They review each other's PRs, publish, plan. And they rot. I watched every flavor of it, then built the countermeasures.&lt;/p&gt;

&lt;h2&gt;
  
  
  What context rot is
&lt;/h2&gt;

&lt;p&gt;Four failures that compound.&lt;/p&gt;

&lt;p&gt;Compression loss. When a session runs out of window, a summary replaces raw history. It sounds fine until the agent needs the detail it summarized away. I watched an agent repeat a plan that contradicted a decision two screens earlier. Nobody noticed, because the contradiction lived at a depth that was gone.&lt;/p&gt;

&lt;p&gt;Drift. Each turn pulls the agent toward the recent and the loud. Given ten pieces of context, it weights the last five. Over hundreds of turns it quietly solves a different problem than the one you assigned.&lt;/p&gt;

&lt;p&gt;Priority blurring. "We decided X" and "we looked at X" feel the same in the tokens. Decisions stop being binding and become suggestions.&lt;/p&gt;

&lt;p&gt;Noise accretion. Stale output and dead ends pile up. The signal-to-noise ratio falls, and a degraded ratio looks like a dumber model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody warns you about
&lt;/h2&gt;

&lt;p&gt;The cheap fix is "just enlarge the window." I built that reflex out of myself. Longer windows don't fix rot, they defer it — later in the timeline, larger cost per turn, and past some point the model does visibly worse than with a lean window. Remembering everything is a form of forgetting: the important thing drowns.&lt;/p&gt;

&lt;p&gt;And rot is per-profile, not per-agent. In a team sharing a codebase, what agent A rotted away still matters to agent B. So the fix must live outside any single session. Memory in one agent's head dies with that session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works
&lt;/h2&gt;

&lt;p&gt;None of this is exotic.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Summarize to executable facts, not prose. Emit decisions and constraints as explicit, fixed-schema entries the agent can act on. A good summary answers "what is still binding" before "what happened."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Move memory out of the session into a semantic index. Our biggest win. Keep lessons and decisions in a plain markdown vault, index with embeddings, and search returns meaning, not exact strings. "Offer-notification" and "pet-booking" are unrelated strings and near-identical problems; string search never connects them, the graph does — in milliseconds, on one SQLite file and a small local embedding model. No vector DB, no cluster, no bill. Don't ask one long-lived brain to do all the retrieval.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn knowledge into skills, not notes. A lesson read once and never referenced is a lesson that rots. The durable form is a skill: a procedure loaded on demand behind a trigger, pulled into context exactly when it applies and out the rest of the time. That's the difference between a memory of what to do and a memory of when to use it. The trigger is the part to get right.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Layer memory by half-life. Instant preferences live in a compact always-on store; entity facts in a structured store you can query; long-lived conventions in a vault you load on purpose. Each level has its own audit cadence, so the always-on layer never bloats and the deep layer never goes stale silently. Cap and prune the always-on store on a schedule: an always-on memory that grows forever is context rot in slow motion.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Patterns to rot-proof an agent
&lt;/h2&gt;

&lt;p&gt;Whatever you pick, these hold.&lt;/p&gt;

&lt;p&gt;Make forgetting visible. If a session drops something, drop it loudly: what entered context, what left it, why. You can't manage decay you can't see.&lt;/p&gt;

&lt;p&gt;Bind decisions harder than facts. A decision needs a source, a status, an owner. A degraded decision is an incident, not a nuisance.&lt;/p&gt;

&lt;p&gt;Retrieve, don't carry. Pull the relevant slice on demand instead of carrying a fat context everywhere. The leaner the working set, the slower the rot.&lt;/p&gt;

&lt;p&gt;One source of truth for shared rules. When agents cooperate, conventions belong in one place anyone can read, not duplicated into private memory where they rot out of sync and you end up fighting three versions of the same rule.&lt;/p&gt;

&lt;p&gt;Treat memory as code. Schema changes, evals, tests. The vault is source, the index is a produced artifact: rebuild it on a cron and never hand-edit it. A knowledge graph you can't rebuild from source is a liability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable take
&lt;/h2&gt;

&lt;p&gt;You never beat context rot. You manage it. Design so the cost of rotting is contained and observable, and so the source of truth survives the session because it lives outside it.&lt;/p&gt;

&lt;p&gt;Our agents now run week-long cycles with far less decay than the first version at hour three. The difference was never a better model. It was admitting that an agent's context is a perishable working surface, not a mind, and engineering it that way.&lt;/p&gt;

&lt;p&gt;I keep getting asked which framework solves this. The answer stays boring: write good compression, index for meaning, encode lessons as triggered skills, cap the always-on memory, and keep long-lived truth somewhere the agent can query and rebuild instead of carry. The agent will look cooler carrying everything. It will also rot faster.&lt;/p&gt;

&lt;p&gt;So: how do you decide what an agent is allowed to forget, and how do you make sure it tells you when it does?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
      <category>agents</category>
    </item>
    <item>
      <title>When Should You NOT Use an Agent?</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Thu, 27 Aug 2026 19:21:36 +0000</pubDate>
      <link>https://dev.to/danilgaleev/when-should-you-not-use-an-agent-4bk2</link>
      <guid>https://dev.to/danilgaleev/when-should-you-not-use-an-agent-4bk2</guid>
      <description>&lt;p&gt;Everyone is asking "should we use agents?" The real question is "when should we NOT?"&lt;/p&gt;

&lt;p&gt;I keep seeing teams bolt an agent on because it's the hot thing — then discover they reinvented a state machine with worse debugging. Agents don't solve a problem by existing. They are a mechanism for &lt;em&gt;deferring decisions to a runtime&lt;/em&gt;. When your inputs, tools, and failure modes are well-understood, that deferral buys you nothing but nondeterminism.&lt;/p&gt;

&lt;p&gt;The architecture question is not "LLM or not." It is: &lt;strong&gt;where does the judgment boundary sit?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three places the boundary lands
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A single agent is a program.&lt;/strong&gt; For a well-scoped task with a known toolset, you don't need a loop at all. You need a deterministic pipeline — with the LLM as one component, not the orchestrator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The moment you need a loop, you're building a runtime.&lt;/strong&gt; A runtime is a different beast. It has observability, tool permissions, credit and rate limits, and a way to explain what it did after the fact. That's not "more AI." That's distributed systems with a language model as the cognitive layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The expensive failure is capability you never signed up for.&lt;/strong&gt; Agents surface things you didn't design for: open-ended tool calls, emergent side-effects, scale that hits budgets or audit. This is where maturity shows — not in the cleverness of the model, but in the &lt;em&gt;constraints&lt;/em&gt; around it: entitlements, approval, observability, evals that cover the failure path, not just the happy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tell
&lt;/h2&gt;

&lt;p&gt;Most of what teams call "agent architecture" is a decision-making boundary placed in the wrong spot, papered over with more layers. If you reach for a framework, an orchestrator, a runtime — stop and ask what you're actually deferring, and whether that deferral is working or just making the system harder to debug.&lt;/p&gt;

&lt;p&gt;A few heuristics I use before building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Can the task be expressed as steps with a known order?&lt;/strong&gt; Pipeline with the LLM as a step — not an agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the agent invoke known tools with expected outputs?&lt;/strong&gt; A single governed agent, thin on top of a deterministic core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you need to route mid-task to unexpected states?&lt;/strong&gt; Only then a real runtime, and only if you accept owning its observability, limits, and audit trail as first-class work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the failure non-recoverable?&lt;/strong&gt; Then don't put an agent in the loop at all. A wrong tool call inside a runtime can hurt you faster than a slower deterministic path ever will.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Constrain before you automate. The agent looks cooler; it's also much harder to explain three months from now.&lt;/p&gt;

&lt;p&gt;So: when you're scoping a new system, what makes you reach for a deterministic pipeline instead of an agent — or the other way? I'd like the rules teams actually run with, not the ones they present in talks.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>agents</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
