<?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: Barry Xiong</title>
    <description>The latest articles on DEV Community by Barry Xiong (@barryxiong).</description>
    <link>https://dev.to/barryxiong</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%2F4051679%2F1769d523-021c-44f4-9a4c-c4a5a903f948.jpg</url>
      <title>DEV Community: Barry Xiong</title>
      <link>https://dev.to/barryxiong</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/barryxiong"/>
    <language>en</language>
    <item>
      <title>Building a Reliable AI Image Pipeline: Tasks, Failures, and Credit Refunds</title>
      <dc:creator>Barry Xiong</dc:creator>
      <pubDate>Thu, 06 Aug 2026 15:36:19 +0000</pubDate>
      <link>https://dev.to/barryxiong/building-a-reliable-ai-image-pipeline-tasks-failures-and-credit-refunds-19ig</link>
      <guid>https://dev.to/barryxiong/building-a-reliable-ai-image-pipeline-tasks-failures-and-credit-refunds-19ig</guid>
      <description>&lt;p&gt;Most AI image generators look like a prompt box with a &lt;strong&gt;Generate&lt;/strong&gt; button. That is also how my first version started.&lt;/p&gt;

&lt;p&gt;But once real users entered the workflow, the difficult problems appeared somewhere else: browser refreshes, external task IDs, reference images, partial failures, credit refunds, private assets, and public artwork moderation.&lt;/p&gt;

&lt;p&gt;While building &lt;a href="https://magggic.com/" rel="noopener noreferrer"&gt;Magggic&lt;/a&gt;, I learned that an AI image generator is less like a form submission and more like a small distributed job system. This article covers the decisions that made that workflow more reliable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code samples below are intentionally simplified. The important part is the shape of the workflow, not a specific database or image provider.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The prompt box is only the beginning
&lt;/h2&gt;

&lt;p&gt;A synchronous prototype is easy to imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That version works until the request takes a minute, the provider times out, one of four requested images fails, or the user refreshes the page.&lt;/p&gt;

&lt;p&gt;The production workflow I needed looked more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt + references
        ↓
Create a local queued task
        ↓
Charge credits with an idempotency key
        ↓
Submit work to the image provider
        ↓
Persist every completed output immediately
        ↓
Finalize the task and refund failed outputs
        ↓
Keep the result private until the user publishes it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider request is only one step. The local task is the source of truth for what the user sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Persist the task before calling the provider
&lt;/h2&gt;

&lt;p&gt;The first important decision was to create a generation record before making the external API request.&lt;/p&gt;

&lt;p&gt;A generation stores the information needed to reconstruct the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Generation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;referenceImages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&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;queued&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generating&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;providerRequestIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;failureReason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;Persisting first gives the UI a stable generation ID immediately. The browser can refresh, reconnect, or open the task from history without depending on the original HTTP connection.&lt;/p&gt;

&lt;p&gt;It also gives operational errors a useful identity. “The provider timed out” is difficult to investigate. “Generation &lt;code&gt;abc123&lt;/code&gt; timed out after provider task &lt;code&gt;xyz789&lt;/code&gt;” is actionable.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make state transitions explicit
&lt;/h2&gt;

&lt;p&gt;Generation states should not be decorative labels. They should control which writes are valid.&lt;/p&gt;

&lt;p&gt;For example, a worker should only start a queued task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateGeneration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;generationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;nextStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generating&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;started&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Likewise, an output should only be attached while the task is generating. Final completion should fail loudly if the task is already in an unexpected state.&lt;/p&gt;

&lt;p&gt;This avoids two common problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;two workers processing the same task;&lt;/li&gt;
&lt;li&gt;a late provider response overwriting a task that has already failed or completed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I prefer conditional updates over reading a status and then updating it later. The condition and the transition happen together, which makes races much easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Persist each output as soon as it succeeds
&lt;/h2&gt;

&lt;p&gt;When a user requests four images, the provider may effectively give you four independent outcomes. Waiting for every result before saving anything creates unnecessary risk.&lt;/p&gt;

&lt;p&gt;The workflow now persists each completed image as it arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;persistCompletedOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProviderOutput&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;storedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;storeImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;appendGenerationOutput&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;generationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expectedStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generating&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outputIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;storedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;externalTaskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskId&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;This produces three useful properties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A successful image is not lost because another image failed later.&lt;/li&gt;
&lt;li&gt;Progress is based on real persisted outputs, not an invented percentage.&lt;/li&gt;
&lt;li&gt;The UI can show completed images while the remaining outputs are still running.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is one uncomfortable edge case: storage succeeds but the database write fails. In that case, the stored object should be deleted or recorded for cleanup. Otherwise every database outage can leave orphaned files behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat partial success as a real outcome
&lt;/h2&gt;

&lt;p&gt;The easiest implementation treats a batch as either successful or failed. That is also the least useful implementation for the user.&lt;/p&gt;

&lt;p&gt;Suppose the user requests four images:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;three images complete successfully;&lt;/li&gt;
&lt;li&gt;one provider task fails;&lt;/li&gt;
&lt;li&gt;the total generation price was 32 credits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Marking the whole task as failed hides three valid outputs. Marking it as fully successful charges the user for something they did not receive.&lt;/p&gt;

&lt;p&gt;The better result is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status: completed
successful outputs: 3
failed outputs: 1
credits refunded: 8
failure note: one output could not be completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In simplified form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;failedCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isRejected&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refundAmount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;failedCount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;unitCost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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;await&lt;/span&gt; &lt;span class="nf"&gt;refundCredits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refundAmount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;completeGeneration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;completedOutputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;failureReason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;failedCount&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;buildPartialFailureMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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 final state can still be &lt;code&gt;completed&lt;/code&gt; because usable outputs exist. The partial-failure message explains why fewer images were returned and how many credits were refunded.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Credits need a ledger, not only a balance
&lt;/h2&gt;

&lt;p&gt;At first, a credit system looks like one integer on the user record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That stops being sufficient as soon as you need to answer basic support questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why did this balance change?&lt;/li&gt;
&lt;li&gt;Was this generation charged twice?&lt;/li&gt;
&lt;li&gt;Was a failed task refunded?&lt;/li&gt;
&lt;li&gt;Which type of credits were consumed?&lt;/li&gt;
&lt;li&gt;What was the balance after the operation?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The approach I settled on keeps both current balances and an append-only ledger.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CreditLedgerEntry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expire&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adjustment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;balanceAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;referenceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;referenceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every spend or refund has a unique idempotency key. Retrying the same completion handler cannot create a second refund.&lt;/p&gt;

&lt;p&gt;This is especially important with asynchronous providers. Network timeouts encourage retries, and retries are exactly where accidental double charges and double refunds appear.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Prompt remix and reference reuse are different operations
&lt;/h2&gt;

&lt;p&gt;An image detail page often needs two actions that look similar but have different semantics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create similar&lt;/strong&gt; reuses the prompt and generation parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use as reference&lt;/strong&gt; sends the current image back as a visual input.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combining them into one generic “Remix” action makes the data ambiguous. A future reviewer cannot tell whether the result came from text alone or from another image.&lt;/p&gt;

&lt;p&gt;I now store reference images on the generation task and carry a separate reference-image status onto a published artwork. For manually imported artwork, the status can be &lt;code&gt;used&lt;/code&gt;, &lt;code&gt;not_used&lt;/code&gt;, or &lt;code&gt;unknown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This provenance matters for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The UI can explain what another user needs to reproduce the result.&lt;/li&gt;
&lt;li&gt;Public pages do not need to expose the original private reference file.&lt;/li&gt;
&lt;li&gt;Moderation and support can distinguish text-to-image from image-guided generation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  7. A generated asset is not automatically a public artwork
&lt;/h2&gt;

&lt;p&gt;Another early design mistake is treating every generated image as public content.&lt;/p&gt;

&lt;p&gt;Generation outputs belong in the user's private assets and history. Publishing is a separate action with a separate lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ArtworkStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;withdrawn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The artwork record can reference a generation output, but it also owns public metadata such as category, tags, publication time, moderation state, and the public image URL.&lt;/p&gt;

&lt;p&gt;This separation keeps several product rules simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refreshing generation history never publishes anything.&lt;/li&gt;
&lt;li&gt;A user can submit only the output they choose.&lt;/li&gt;
&lt;li&gt;Moderation can reject or hide a public artwork without deleting the private generation.&lt;/li&gt;
&lt;li&gt;Public &lt;code&gt;Latest&lt;/code&gt; and &lt;code&gt;Popular&lt;/code&gt; feeds operate on approved artwork, not raw generation history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also prevents a privacy problem: a successful provider response should never equal automatic consent to publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Design failure states as part of the product
&lt;/h2&gt;

&lt;p&gt;Failure UI is often added at the end, but it is part of the core workflow.&lt;/p&gt;

&lt;p&gt;A useful failed task should tell the user:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what happened in plain language;&lt;/li&gt;
&lt;li&gt;whether any outputs were saved;&lt;/li&gt;
&lt;li&gt;whether credits were refunded;&lt;/li&gt;
&lt;li&gt;whether retrying will create a new task;&lt;/li&gt;
&lt;li&gt;where the failed attempt remains visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep failed records in generation history instead of removing them. Retrying creates a new history item rather than rewriting the old one. That gives both the user and the support team a truthful timeline.&lt;/p&gt;

&lt;p&gt;The same rule applies to progress. If the provider only gives a queued state and a completed result, the UI should show those real states. A beautiful fake “73%” progress bar is still fake.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do from the first day now
&lt;/h2&gt;

&lt;p&gt;If I were starting the generation workflow again, I would make these decisions before polishing the prompt box:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a persistent local task before calling any provider.&lt;/li&gt;
&lt;li&gt;Use conditional state transitions for workers and result writes.&lt;/li&gt;
&lt;li&gt;Persist each output independently.&lt;/li&gt;
&lt;li&gt;Model partial success instead of flattening every batch into pass or fail.&lt;/li&gt;
&lt;li&gt;Record credits in an idempotent ledger.&lt;/li&gt;
&lt;li&gt;Keep text remix and image reference reuse separate.&lt;/li&gt;
&lt;li&gt;Separate private generated assets from moderated public artwork.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these ideas is limited to image generation. Any AI product that combines asynchronous providers, paid usage, and persistent outputs will eventually face the same questions.&lt;/p&gt;

&lt;p&gt;The prompt box is the visible part. Reliability is the product underneath it.&lt;/p&gt;

&lt;p&gt;I would be interested to hear how other developers model partial AI task failures, provider retries, and idempotent refunds.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>How I model scientific claims before generating a diagram</title>
      <dc:creator>Barry Xiong</dc:creator>
      <pubDate>Tue, 28 Jul 2026 16:05:57 +0000</pubDate>
      <link>https://dev.to/barryxiong/how-i-model-scientific-claims-before-generating-a-diagram-mlo</link>
      <guid>https://dev.to/barryxiong/how-i-model-scientific-claims-before-generating-a-diagram-mlo</guid>
      <description>&lt;p&gt;A prompt I keep using when I test scientific image models is short: "Draw the MAPK pathway."&lt;/p&gt;

&lt;p&gt;That sentence leaves most of the important decisions unstated. Which branch matters? Which cell type are we talking about? Does an arrow mean activation, transport, or simply "this happens next"? Is the figure explaining a known mechanism or summarizing one experiment?&lt;/p&gt;

&lt;p&gt;A model can make those missing decisions on its own. The result may look polished and still be scientifically useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The picture is the last step
&lt;/h2&gt;

&lt;p&gt;When I started working on text-to-figure generation, I spent most of my time thinking about rendering. Line quality, spacing, color, export size. Within a week, I was spending more time checking arrows than pixels.&lt;/p&gt;

&lt;p&gt;The useful intermediate artifact turned out to be a small figure specification. Mine often starts this simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Show how MEK inhibition changes downstream ERK signaling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"MEK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ERK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transcription factor"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"relations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MEK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ERK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"activates"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"literature plus this experiment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"unmeasured branches"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unlisted proteins"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a production schema. It is a quick way to expose decisions before a model hides them under attractive pixels.&lt;/p&gt;

&lt;p&gt;If I cannot write the purpose in one sentence, the figure usually has too much work to do. Shrinking the labels will not fix that. I split the figure instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arrows need types
&lt;/h2&gt;

&lt;p&gt;An arrow is one of the easiest shapes to draw and one of the easiest ways to make a false claim.&lt;/p&gt;

&lt;p&gt;In a workflow, an arrow may mean sequence. In a pathway, it may mean activation or inhibition. In an equipment diagram, it may mean material flow. Those meanings should not be interchangeable just because they share the same SVG path.&lt;/p&gt;

&lt;p&gt;For a developer, the relation deserves its own type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;activates&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inhibits&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-step&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;measured&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;literature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interpretation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type does two useful things. It lets the renderer choose a visual convention, and it gives the validator something concrete to inspect. A vague "connect these nodes" instruction gives us neither.&lt;/p&gt;

&lt;p&gt;I also keep relation meaning out of color whenever possible. Color disappears in grayscale printing, on poor projectors, and for some readers with color-vision differences. Arrowheads, line styles, and labels carry the meaning. Color can help, but it should not be the only carrier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence and explanation need different treatment
&lt;/h2&gt;

&lt;p&gt;Scientific papers mix several kinds of visuals. A chart presents measurements. A microscopy image records an observation. A mechanism diagram explains how the author thinks pieces fit together.&lt;/p&gt;

&lt;p&gt;The trouble starts when a generated illustration borrows the visual authority of measured data.&lt;/p&gt;

&lt;p&gt;A detailed receptor model can imply structural precision that was never measured. A realistic anatomical cutaway can look patient-specific even when it is generic. The more convincing the render, the easier it is to miss that distinction.&lt;/p&gt;

&lt;p&gt;I now attach a source category to anything that enters a figure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;measured in the current work&lt;/li&gt;
&lt;li&gt;supported by cited literature&lt;/li&gt;
&lt;li&gt;interpretation or hypothesis&lt;/li&gt;
&lt;li&gt;orientation only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those categories can change line weight, labels, captions, and citations. They also make review easier. A co-author can disagree with the category without arguing about the entire composition.&lt;/p&gt;

&lt;p&gt;For explanatory figures, I often prefer plain line art to a cinematic render. Lower visual detail leaves less room for accidental claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  My validation loop is deliberately boring
&lt;/h2&gt;

&lt;p&gt;I used to regenerate a figure until one version felt right. That loop rewards appearance. A clean layout can distract me from a wrong connection.&lt;/p&gt;

&lt;p&gt;Now I check four things in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the topology correct?&lt;/li&gt;
&lt;li&gt;Do the labels match the paper or source material?&lt;/li&gt;
&lt;li&gt;Can I explain what every arrow means?&lt;/li&gt;
&lt;li&gt;Is the figure readable at its final size?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A typo is usually a local edit. A topology error sends the figure back to the specification. There is no point tuning shadows on the wrong graph.&lt;/p&gt;

&lt;p&gt;The final-size check catches more problems than I expected. A figure that looks comfortable in a full browser window may collapse inside a two-column paper. I test the exported size, not the editor view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where generation fits
&lt;/h2&gt;

&lt;p&gt;Generative tools are useful for mechanism sketches, experimental workflows, equipment concepts, and early graphical-abstract drafts. They reduce the cost of getting from an empty canvas to something a team can discuss.&lt;/p&gt;

&lt;p&gt;They are a poor substitute for data-driven charts, original microscopy, diagnostic images, or exact chemical structures. Those visuals should come from the underlying data or specialist software. A generated image may explain evidence, but it should not impersonate it.&lt;/p&gt;

&lt;p&gt;I eventually built parts of this workflow into &lt;a href="https://scientificfiguregenerator.com/" rel="noopener noreferrer"&gt;Scientific Figure Generator&lt;/a&gt;. It can produce line art, flat illustrations, and 3D-style drafts from text. I still treat every result as a draft, including the ones that look finished.&lt;/p&gt;

&lt;p&gt;The product question I keep coming back to is how much structure to expose. Too little invites invented relationships. Too much turns a quick visual task into a long form with dozens of fields. I do not think that tradeoff has a universal answer.&lt;/p&gt;

&lt;p&gt;For now, my rule is simple: before a generated diagram reaches a paper or presentation, every relationship in it needs a source. If I cannot find one, the figure is not ready.&lt;/p&gt;

&lt;p&gt;How are you handling semantic validation when AI generates diagrams in your own tools?&lt;/p&gt;




&lt;p&gt;Disclosure: I used an AI writing assistant to help edit the English and tighten this draft. The examples, workflow, and product decisions come from my work building scientific-visualization tools.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>science</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
