<?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: Kunwar Harshit</title>
    <description>The latest articles on DEV Community by Kunwar Harshit (@hrshitkunwartech).</description>
    <link>https://dev.to/hrshitkunwartech</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%2F4118165%2F9ae2f884-dc5f-4b17-88a2-1c6202005d17.png</url>
      <title>DEV Community: Kunwar Harshit</title>
      <link>https://dev.to/hrshitkunwartech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hrshitkunwartech"/>
    <language>en</language>
    <item>
      <title>Your integration is running as an admin. Nobody remembers approving that.</title>
      <dc:creator>Kunwar Harshit</dc:creator>
      <pubDate>Tue, 15 Sep 2026 10:35:32 +0000</pubDate>
      <link>https://dev.to/hrshitkunwartech/your-integration-is-running-as-an-admin-nobody-remembers-approving-that-5ei3</link>
      <guid>https://dev.to/hrshitkunwartech/your-integration-is-running-as-an-admin-nobody-remembers-approving-that-5ei3</guid>
      <description>&lt;p&gt;It happens in about forty seconds, usually on a Thursday.&lt;/p&gt;

&lt;p&gt;You are wiring a sync in a sandbox. The write fails on a permission you did not expect. You have three other things to do that day, so you grant the integration user a broader role to get past it, make a mental note to narrow it later, and ship.&lt;/p&gt;

&lt;p&gt;Nobody narrows it later. Narrowing it has no deadline, no ticket, and no owner, and the integration works, which is the whole problem. Working software generates no pressure to change.&lt;/p&gt;

&lt;p&gt;Two years on, that account can read every record in the system, write to objects the sync has never touched, and in a few cases delete. It has no password rotation, no expiry, and a name like &lt;code&gt;api_user_2&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that is actually new
&lt;/h2&gt;

&lt;p&gt;Over-privileged service accounts are an old problem, and the old advice has been available for decades. What changed is the volume and the attack surface.&lt;/p&gt;

&lt;p&gt;CyberArk's 2025 Identity Security Landscape, a survey of 2,600 security decision-makers, put machine identities at &lt;strong&gt;82 for every human identity&lt;/strong&gt;, with 42% of them holding privileged or sensitive access. In the same survey, &lt;strong&gt;88% of organisations still define "privileged user" as a human being.&lt;/strong&gt; So the majority of privileged accounts in a typical company sit outside the definition that governs privileged accounts.&lt;/p&gt;

&lt;p&gt;Then agents arrived, and they differ from a cron job in one way that matters here: their instructions come from content they read at runtime. A scheduled sync does the same thing every night. An agent summarising an inbox is executing partly on the contents of that inbox, and anyone who can get text in front of it gets a vote. That is not a hypothetical risk class, it is the first entry in the OWASP LLM Top 10.&lt;/p&gt;

&lt;p&gt;An over-privileged cron job is a bug waiting for an edge case. An over-privileged agent is a bug waiting for someone to write it a message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things that break, in order of how quietly they break
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Blast radius.&lt;/strong&gt; The sync was supposed to update one field on one object. With a broad role, a bad loop can touch anything the account can reach. The permission set is the only thing standing between a logic error and your whole database, and you removed it on a Thursday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attribution disappears.&lt;/strong&gt; If three workflows share &lt;code&gt;api_user_2&lt;/code&gt;, your audit log records that &lt;code&gt;api_user_2&lt;/code&gt; changed a close date. It cannot tell you which workflow, which run, or what triggered it. You are now debugging by correlating timestamps, which is the worst kind of Tuesday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You cannot revoke one thing.&lt;/strong&gt; Shared credentials mean revocation is all or nothing. Something misbehaves, you check what else uses that account, discover it is four things including one finance cares about, and you do not revoke it. You open a Slack thread instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credentials outlive their purpose.&lt;/strong&gt; The workflow gets deprecated. The key does not. It sits there, valid, attached to a system nobody runs, until an audit finds it or something worse does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is one identity per workflow
&lt;/h2&gt;

&lt;p&gt;Not one per integration, and definitely not one per company. One per workflow, scoped to exactly the objects and fields that workflow touches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  # Before: one account, everything, forever
&lt;span class="gd"&gt;- scopes: [ crm.objects.contacts.*, crm.objects.deals.*,
-            crm.objects.companies.*, tickets.*, files.*, settings.* ]
- owner: (unset)
- expires: never
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  # After: one per workflow, scoped to what it actually does
&lt;span class="gi"&gt;+ workflow: post-call-crm-update
+ scopes:  [ crm.objects.deals.read,
+            crm.objects.deals.write:close_date,
+            crm.objects.deals.write:stage ]
+ owner:   platform-team
+ expires: 90d
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mechanics exist in every platform worth integrating with. Salesforce gives you permission sets and field-level security, so an integration user can be granted edit on two fields rather than an object. HubSpot private apps take granular scopes rather than a role. Most OAuth providers let you request narrow scopes and simply are not asked to.&lt;/p&gt;

&lt;p&gt;Three rules make it stick:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope to fields, not objects, wherever the platform allows it.&lt;/strong&gt; "Can edit Opportunity" and "can edit Opportunity.CloseDate" are different blast radii, and the second one is usually what you meant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give every identity a human owner and an expiry.&lt;/strong&gt; An account nobody owns is an account nobody revokes. An expiry forces a five-minute review you would otherwise never schedule, and if renewing it is annoying, that is the system telling you something true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put the workflow name in the identity.&lt;/strong&gt; &lt;code&gt;post-call-crm-update&lt;/code&gt; in an audit log answers the question. &lt;code&gt;api_user_2&lt;/code&gt; starts an investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;Pick your most recently shipped integration and answer this: can you turn it off, right now, without a meeting?&lt;/p&gt;

&lt;p&gt;If revoking one workflow's access means checking what else breaks, you do not have scoped identities. You have a shared admin account with extra steps, and every agent you point at it inherits the whole thing.&lt;/p&gt;

&lt;p&gt;SailPoint's 2025 survey found only &lt;strong&gt;44% of organisations have any policy for securing the AI agents they already run&lt;/strong&gt;. The gap is not usually knowledge. It is that scoping permissions produces nothing you can demo, and Thursday you had three other things to do.&lt;/p&gt;




&lt;p&gt;The longer version of this argument, including how per-workflow identity makes writes attributable and reversible rather than just contained, is written up here: &lt;a href="https://mindlyft.in/docs/scoped-agent-identity" rel="noopener noreferrer"&gt;giving each agent its own scoped identity&lt;/a&gt;. The vocabulary for the account type itself is &lt;a href="https://mindlyft.in/glossary/non-human-identity" rel="noopener noreferrer"&gt;non-human identity&lt;/a&gt;, which is worth knowing because it is what the tooling and the compliance frameworks call it.&lt;/p&gt;

&lt;p&gt;I build approval and audit layers over agents that write to production systems at &lt;a href="https://mindlyft.in" rel="noopener noreferrer"&gt;Mindlyft&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What is the broadest permission you have found on an account nobody could explain? Mine was a reporting integration with delete.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>api</category>
      <category>devops</category>
      <category>integration</category>
    </item>
    <item>
      <title>Your integration returned 200. The record didn't change.</title>
      <dc:creator>Kunwar Harshit</dc:creator>
      <pubDate>Tue, 15 Sep 2026 10:23:50 +0000</pubDate>
      <link>https://dev.to/hrshitkunwartech/your-integration-returned-200-the-record-didnt-change-5ggo</link>
      <guid>https://dev.to/hrshitkunwartech/your-integration-returned-200-the-record-didnt-change-5ggo</guid>
      <description>&lt;p&gt;Every cross-system integration I have debugged in the last two years failed the same way, and it was never the way the logs suggested.&lt;/p&gt;

&lt;p&gt;The logs said success. The API returned &lt;code&gt;200&lt;/code&gt;. The retry counter was zero. And the field in the target system still held the old value.&lt;/p&gt;

&lt;p&gt;This is not an edge case. It is the default behaviour of every major CRM and ticketing API, and if your pipeline treats a &lt;code&gt;2xx&lt;/code&gt; as proof of a write, you have a silent data-loss bug you have not found yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 2xx means "accepted", not "stored"
&lt;/h2&gt;

&lt;p&gt;That distinction sounds pedantic until it costs you a quarter of pipeline data. Here are the five mechanisms I keep hitting, roughly in order of how often they bite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Partial success inside a successful response.&lt;/strong&gt; Bulk and composite endpoints are the worst offenders. Salesforce's composite API with &lt;code&gt;allOrNone: false&lt;/code&gt; returns &lt;code&gt;200 OK&lt;/code&gt; with a body containing per-record failures. If you check the HTTP status and move on, you have just discarded the error.&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="c1"&gt;# This is the bug.
&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;composite_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# 200. Looks fine.
&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;synced %d records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

&lt;span class="c1"&gt;# The failures were in here all along.
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record %s: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Downstream automation overwrites you milliseconds later.&lt;/strong&gt; Your write lands. Then a workflow rule, a Flow, or a HubSpot workflow fires on that same record change and sets the field back, or to something else entirely. Your write succeeded and was immediately undone. Nothing in your logs will show it, because from your side nothing went wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Duplicate and merge rules move the record out from under you.&lt;/strong&gt; You write to record &lt;code&gt;A&lt;/code&gt;. A dedupe rule merges &lt;code&gt;A&lt;/code&gt; into &lt;code&gt;B&lt;/code&gt;. Your value is now either on a record you were not targeting or gone. The API still told you &lt;code&gt;200&lt;/code&gt;, because at the moment it answered, it was true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Field-level permissions drop fields silently.&lt;/strong&gt; The integration user lacks edit access on one field in a twelve-field update. Depending on the endpoint, you get a &lt;code&gt;200&lt;/code&gt; and eleven fields written. The twelfth is simply absent from the result, not reported as an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Eventual consistency means read-your-write is not guaranteed.&lt;/strong&gt; Read back too fast and you get the pre-write value, which sends you chasing a bug that does not exist. This one produces false alarms rather than silent failures, which is why it is the least dangerous and the most annoying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is boring: read it back
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_and_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write, then confirm the target system actually holds the values.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;        &lt;span class="c1"&gt;# back off for #5
&lt;/span&gt;        &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                 &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Verified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Do NOT retry the write. Something is actively rejecting or
&lt;/span&gt;    &lt;span class="c1"&gt;# overwriting it, and hammering the endpoint will not change that.
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;VerificationFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things matter more than the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify the fields you care about, not all of them.&lt;/strong&gt; A full record comparison will drift constantly on system-managed fields like &lt;code&gt;LastModifiedDate&lt;/code&gt; and produce alerts nobody reads. Pick the fields whose wrongness would actually cost something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never auto-retry a failed verification.&lt;/strong&gt; A failed verify means something is rejecting or overwriting your write. Retrying makes that happen again, faster. Escalate to a human with the diff: expected, actual, record, timestamp. A three-line diff is a five-minute fix. A retry loop is a Tuesday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log the diff, not the outcome.&lt;/strong&gt; &lt;code&gt;verification failed&lt;/code&gt; tells you nothing at 2am. &lt;code&gt;close_date expected 2026-03-31, actual 2026-06-30, record 0061x…, 340ms after write&lt;/code&gt; tells you a Flow is overwriting close dates, which is a completely different problem than a permissions error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the verify step is the one that gets skipped
&lt;/h2&gt;

&lt;p&gt;Because it is the only stage with no visible output when it works. Trigger, join, transform and write all produce something you can point at in a demo. Verification produces silence, and silence does not demo well.&lt;/p&gt;

&lt;p&gt;It is also the stage that separates an automation from a system. A zap moves a record. A system proves the record arrived and tells you when it did not. That gap is where most "our data is a mess" complaints actually originate, long after anyone remembers which integration caused it.&lt;/p&gt;

&lt;p&gt;If you want the wider version of this argument, the discipline of building revenue workflows this way now has a name and a job market attached to it: &lt;a href="https://mindlyft.in/resources/gtm-engineering" rel="noopener noreferrer"&gt;GTM engineering&lt;/a&gt;. The write-verify pattern above is stage five of five in how those workflows get built, and it is the stage almost every tutorial leaves out.&lt;/p&gt;

&lt;p&gt;I build these systems at &lt;a href="https://mindlyft.in" rel="noopener noreferrer"&gt;Mindlyft&lt;/a&gt;. Half of what we ship is not the automation. It is the proof that the automation did what it said.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What is the worst silent-write failure you have found? I am collecting the mechanisms, and I suspect five is not the complete list.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>automation</category>
      <category>integration</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The approval gate: a pattern for AI agents that write to production systems</title>
      <dc:creator>Kunwar Harshit</dc:creator>
      <pubDate>Wed, 09 Sep 2026 20:34:38 +0000</pubDate>
      <link>https://dev.to/hrshitkunwartech/the-approval-gate-a-pattern-for-ai-agents-that-write-to-production-systems-471f</link>
      <guid>https://dev.to/hrshitkunwartech/the-approval-gate-a-pattern-for-ai-agents-that-write-to-production-systems-471f</guid>
      <description>&lt;p&gt;Most AI agent demos end at the moment the agent decides something. The interesting engineering starts right after that, when the agent writes to a system other people depend on.&lt;/p&gt;

&lt;p&gt;I build automation that writes to CRMs, ticketing systems and inboxes for revenue teams. Those are systems of record. A wrong write is not a bad answer you can regenerate. It is a support ticket, a corrupted forecast, or an email a customer has already read.&lt;/p&gt;

&lt;p&gt;Here is the pattern that survived contact with production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode
&lt;/h2&gt;

&lt;p&gt;The naive design gives the agent credentials and lets it call the API directly.&lt;/p&gt;

&lt;p&gt;This works most of the time. Most of the time is the problem. At 95 percent action accuracy, one in twenty writes is wrong, and those writes compound into a database humans use to make decisions. The cleanup cost per bad write is much higher than the time saved per good write, so the automation goes net negative while still looking successful in your metrics.&lt;/p&gt;

&lt;p&gt;The second order failure is worse. Once users find two or three wrong records they stop trusting the whole dataset and go back to their spreadsheets. Now you have added a system nobody trusts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: propose, diff, approve, execute, log
&lt;/h2&gt;

&lt;p&gt;Split the decision from the side effect. The agent never touches the target API. It emits a proposed action, and a separate executor applies it only after approval.&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;"action_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"act_01H8Z..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crm.opportunity.update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&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;"system"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"salesforce"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Opportunity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0061..."&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;"diff"&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;"StageName"&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;"Discovery"&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;"Proposal"&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;"CloseDate"&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;"2026-10-31"&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;"2026-11-30"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_2291"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"let us aim to get paperwork out next month"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending_approval"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"idempotency_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_2291:opp_0061:stage"&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;Four things make this work in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A diff, not a payload.&lt;/strong&gt; Show the reviewer &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt;. A human can approve or reject a diff in about two seconds. Nobody actually reviews a raw JSON payload, so if you show a payload you have built an approval step that everyone rubber stamps, which is the same as having no gate at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence attached to the claim.&lt;/strong&gt; Every proposed change carries the quote or source that produced it. That is what makes review fast, and what makes the audit log useful six weeks later when someone asks why a field changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency keys.&lt;/strong&gt; Approval is asynchronous and humans double click. Derive the key from the source event plus the target field so a replay is a no-op instead of a duplicate ticket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reversibility.&lt;/strong&gt; Store the &lt;code&gt;from&lt;/code&gt; values, not just the &lt;code&gt;to&lt;/code&gt;. Every applied action gets an inverse. Undo turns a scary automation into one people will actually leave switched on, and it is nearly free to implement once you have captured the diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preflight validation, before a human ever sees it
&lt;/h2&gt;

&lt;p&gt;Do not spend human attention on actions that were going to fail anyway. Before an action enters the approval queue, validate it against the target system. Does the field exist, is the picklist value legal, does the record still exist, does this user have write permission on it.&lt;/p&gt;

&lt;p&gt;Most bad proposals are not judgment errors. They are schema errors, and schema errors are cheap to catch in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to log
&lt;/h2&gt;

&lt;p&gt;Log the whole envelope, not just the outcome. Proposal, evidence, reviewer, decision, timestamp, applied result, and the inverse. If you can answer "who approved this, based on what, and how do I undo it" from a single table, you can pass a security review and you can debug production.&lt;/p&gt;

&lt;p&gt;Do not log the raw customer content that produced the action beyond the specific evidence quote. You want interaction patterns and decisions, not a shadow copy of your customers' data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff
&lt;/h2&gt;

&lt;p&gt;This is slower per action than full autonomy, and that is fine. Reviewing a diff is cheap. Recovering trust after a bad automated write is expensive, and often you do not get the chance.&lt;/p&gt;

&lt;p&gt;Autonomy is the part that demos well. The gate is the part that decides whether the system is still switched on in six months.&lt;/p&gt;

&lt;p&gt;I build this pattern for revenue teams at &lt;a href="https://mindlyft.in" rel="noopener noreferrer"&gt;Mindlyft&lt;/a&gt;, where agents draft the post-call CRM updates, tickets and follow ups, and a human approves before anything ships.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
