<?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: Dmitry Lukashov</title>
    <description>The latest articles on DEV Community by Dmitry Lukashov (@lookatshow).</description>
    <link>https://dev.to/lookatshow</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%2F4186286%2Fc5bebb6e-1841-4308-bcf8-133399f34564.jpg</url>
      <title>DEV Community: Dmitry Lukashov</title>
      <link>https://dev.to/lookatshow</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lookatshow"/>
    <language>en</language>
    <item>
      <title>Four offline checks before giving a marketing agent API access</title>
      <dc:creator>Dmitry Lukashov</dc:creator>
      <pubDate>Fri, 16 Oct 2026 17:51:10 +0000</pubDate>
      <link>https://dev.to/lookatshow/four-offline-checks-before-giving-a-marketing-agent-api-access-2950</link>
      <guid>https://dev.to/lookatshow/four-offline-checks-before-giving-a-marketing-agent-api-access-2950</guid>
      <description>&lt;p&gt;A marketing agent can produce valid JSON and still make the wrong decision. It can average ratios instead of totals, compare packages with different scope, interpret an incomplete CRM response as absence, or turn a local email check into permission to send.&lt;/p&gt;

&lt;p&gt;Here are four small offline exercises I use to make those errors visible before connecting an agent to a live account. Each has synthetic input and a result you can reproduce with Python 3.9+ and the standard library. There is no paid-core dependency or signup.&lt;/p&gt;

&lt;p&gt;These are four independent tools, not an integrated marketing pipeline or a benchmark of any particular model. I maintain the modules as part of Lookatshow. AI assistance was used to prepare this article; the commands below were run against the public artifacts on 16 October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Check the denominator before recommending an ad change
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ilookatshow-gif/direct-report-audit" rel="noopener noreferrer"&gt;Direct report audit&lt;/a&gt; reads a normalized placement CSV or TSV. It does not connect to Yandex Direct.&lt;/p&gt;

&lt;p&gt;Download and unpack the &lt;a href="https://github.com/ilookatshow-gif/direct-report-audit/releases/download/v0.1.0/direct-report-audit-0.1.0.zip" rel="noopener noreferrer"&gt;v0.1.0 ZIP&lt;/a&gt;, open its directory, then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-B&lt;/span&gt; direct_report_audit.py &lt;span class="nt"&gt;--json&lt;/span&gt; audit examples/synthetic-placements.csv &lt;span class="nt"&gt;--max-cpa&lt;/span&gt; 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fictional input contains five rows and four grouped placements. The result has 97 clicks, 6,000 RUB cost and 5 conversions. Total CPA is &lt;strong&gt;1,200.00 RUB&lt;/strong&gt;, calculated from total cost divided by total conversions. Two placements are manual-review candidates under these demonstration thresholds.&lt;/p&gt;

&lt;p&gt;Give the agent a narrower task than "optimize this campaign":&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Recalculate the totals from the fixture. Explain why total CPA is a ratio of sums. List the review candidates and their reasons. Explain which attribution, tracking and sample-size checks would be needed before a campaign change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An attractive recommendation to exclude a placement is not a passing answer. The program has not established causal waste or a complete customer journey. Native export headers and cost units must also be normalized first; the CLI is not a universal parser for every advertising export.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Separate a price change from a package change
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ilookatshow-gif/competitor-offer-radar" rel="noopener noreferrer"&gt;Competitor offer radar&lt;/a&gt; compares supplied local snapshots. It does not scrape websites or verify that an offer was really available.&lt;/p&gt;

&lt;p&gt;Use the &lt;a href="https://github.com/ilookatshow-gif/competitor-offer-radar/archive/7206178f1f6c7484c6f4463c62fe48d497b398d6.zip" rel="noopener noreferrer"&gt;pinned source archive&lt;/a&gt;. This later source revision includes a console-encoding fix; the original v0.1.0 release ZIP predates it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-B&lt;/span&gt; competitor_offer_radar.py compare examples/before.json examples/after.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the synthetic strategy-session package, the price changes from 10,000 to 12,500 RUB: &lt;strong&gt;+25%&lt;/strong&gt;. Delivery changes from four to six business days, and interview quantity changes from two to one. The output includes &lt;code&gt;scope_changed_warning=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Ask the agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Show the arithmetic and identify the changed scope. Can this establish a 25% price increase for equivalent work? What does an offer absent from the later snapshot actually prove?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A useful answer preserves the warning. Missing from a supplied snapshot does not establish that a competitor discontinued the product. Different package content does not support a clean like-for-like price conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Keep incomplete CRM reads out of the absence queue
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ilookatshow-gif/bitrix-read-kit" rel="noopener noreferrer"&gt;Bitrix read kit&lt;/a&gt; has a synthetic demonstration that needs no portal or credentials. Its separate live mode is read-only and limited to what the configured user can see.&lt;/p&gt;

&lt;p&gt;Download the &lt;a href="https://github.com/ilookatshow-gif/bitrix-read-kit/releases/download/v0.1.0/bitrix-read-kit-0.1.0.zip" rel="noopener noreferrer"&gt;v0.1.0 ZIP&lt;/a&gt;, unpack it and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-B&lt;/span&gt; bitrix_read_kit.py demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are five fictional open deals and three unique visible unfinished activities. One deal has a scheduled activity, one an overdue activity, one an undated activity, one a complete activity read with no visible unfinished item, and one an incomplete activity read.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which deal supports an absence claim, and which needs a coverage check? Keep overdue-but-present activities separate from missing ones. State the demo's analysis time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Deal 101 has the complete empty read. Deal 105 has incomplete coverage: its absence status cannot be established. The fixed analysis time is 10 October 2026, 10:00 MSK. Rerunning the fixture today is not a fresh portal capture, and a CRM activity is not proof of an agreed customer next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep a local eligibility check separate from sending
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ilookatshow-gif/unisender-campaign-kit" rel="noopener noreferrer"&gt;UniSender campaign kit&lt;/a&gt; checks a draft, a supplied permission ledger and a suppression file locally. It has no UniSender API connection, contact import or send command.&lt;/p&gt;

&lt;p&gt;Download the &lt;a href="https://github.com/ilookatshow-gif/unisender-campaign-kit/releases/download/v0.1.0/unisender-campaign-kit-0.1.0.zip" rel="noopener noreferrer"&gt;v0.1.0 ZIP&lt;/a&gt;, unpack it and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-B&lt;/span&gt; lookatshow_mail_plan.py demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The six invented rows produce &lt;strong&gt;one eligible-under-supplied-rules row and five excluded rows&lt;/strong&gt;. The reasons are a duplicate address, suppression, expired supplied permission, an unsubscribe flag and no affirmative supplied permission. All addresses use &lt;code&gt;example.invalid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The JSON still reports:&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;"consent_evidence_verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"legal_permission_verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider_acceptance_verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deliverability_verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sending_performed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;Ask the agent to explain those false fields before proposing any next step. A successful command means the local calculation completed. It does not verify a person's actual consent, a provider's acceptance or delivery. The demonstration also uses a fixed evaluation time rather than today's date.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact acceptance prompt
&lt;/h2&gt;

&lt;p&gt;After downloading and reading the four modules' READMEs, try this with your coding agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Run only the four bundled synthetic commands above. For each exercise, report the exact command, exit code, observed values, supported conclusion and one tempting unsupported conclusion. Compare observed values with the stated expectations and report mismatches. Do not connect accounts, fetch customer data, send messages, alter campaigns or upload files. Stop and explain any missing input. Do not present synthetic results as a client case.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The distinction to inspect is between a calculation, the evidence it actually received, and the action someone wants to take. Passing these examples does not establish readiness for a live account. Failing one gives you a specific behavior to fix first.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/ilookatshow-gif/lookatshow-open-tools" rel="noopener noreferrer"&gt;open-tools catalog&lt;/a&gt; links the installation instructions, licenses and limitations. Each module is independently usable for free. There is no hidden telemetry in these offline exercises. If you pass real output to a cloud agent later, that provider's data policy still applies.&lt;/p&gt;

&lt;p&gt;If you try a fixture, which unsupported conclusion did your agent make? A command, environment and synthetic mismatch are useful in a comment or repository issue; please keep customer exports, addresses and credentials private.&lt;/p&gt;

&lt;p&gt;Prepared by Dmitry Lukashov. If your team needs help adapting this kind of evidence check to its own workflow, &lt;a href="https://lookatshow.ru/ai-implementation/?utm_source=devto&amp;amp;utm_medium=expert_article&amp;amp;utm_campaign=four-module-acceptance&amp;amp;utm_content=implementation" rel="noopener noreferrer"&gt;discuss implementation&lt;/a&gt;. Scope and cost are agreed separately from the free tools.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>testing</category>
      <category>showdev</category>
    </item>
    <item>
      <title>An empty CRM response is not evidence of no follow-up</title>
      <dc:creator>Dmitry Lukashov</dc:creator>
      <pubDate>Thu, 15 Oct 2026 08:40:00 +0000</pubDate>
      <link>https://dev.to/lookatshow/an-empty-crm-response-is-not-evidence-of-no-follow-up-3el</link>
      <guid>https://dev.to/lookatshow/an-empty-crm-response-is-not-evidence-of-no-follow-up-3el</guid>
      <description>&lt;p&gt;Suppose your CRM agent returns this sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Two deals have no follow-up. Ask their owners to fix them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The JSON behind it contains two empty arrays. One belongs to a completed activity read. The other belongs to an incomplete read. The agent has turned a collection limit into an accusation.&lt;/p&gt;

&lt;p&gt;You don't fix that with a more persuasive prompt. Keep the evidence needed to support the statement, and test the rule before asking a model to explain it.&lt;/p&gt;

&lt;p&gt;Here's a small, reproducible example using &lt;a href="https://github.com/ilookatshow-gif/bitrix-read-kit/tree/e102c33d20ed5d74591092433e78fb9019e1cc0d?utm_source=devto&amp;amp;utm_medium=technical_article&amp;amp;utm_campaign=bitrix-read-kit&amp;amp;utm_content=source" rel="noopener noreferrer"&gt;Bitrix read kit&lt;/a&gt;, a free MIT-licensed Python CLI from my Lookatshow project. Every record below is synthetic. You need no CRM account, credentials or paid core.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the mistake without connecting a CRM
&lt;/h2&gt;

&lt;p&gt;Use Python 3.9 or later. The package uses the standard library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ilookatshow-gif/bitrix-read-kit.git
&lt;span class="nb"&gt;cd &lt;/span&gt;bitrix-read-kit
git checkout &lt;span class="nt"&gt;--detach&lt;/span&gt; e102c33d20ed5d74591092433e78fb9019e1cc0d
python3 &lt;span class="nt"&gt;-B&lt;/span&gt; bitrix_read_kit.py demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;python&lt;/code&gt; instead of &lt;code&gt;python3&lt;/code&gt; if that's your interpreter name. Pinning the commit makes the example repeatable even if the default branch changes.&lt;/p&gt;

&lt;p&gt;The fixture contains five fictional deals and three unique unfinished activities. Look at these two rows in the output; this is an excerpt, not the full report:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deal_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"activity_read_complete"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"visible_open_activity_ids"&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;"flags"&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;"no_visible_open_activity"&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;"deal_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;105&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"activity_read_complete"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"visible_open_activity_ids"&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;"flags"&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;"activity_coverage_incomplete"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The arrays match. The conclusions don't.&lt;/p&gt;

&lt;p&gt;This tempting filter produces the wrong work queue:&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="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deal_id&lt;/span&gt;&lt;span class="sh"&gt;"&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;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deals&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;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visible_open_activity_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# [101, 105]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A narrower predicate preserves what the reader actually established:&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="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deal_id&lt;/span&gt;&lt;span class="sh"&gt;"&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;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deals&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="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activity_read_complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visible_open_activity_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deal_id&lt;/span&gt;&lt;span class="sh"&gt;"&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;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deals&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;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activity_read_complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# missing: [101]; unknown: [105]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even &lt;code&gt;missing&lt;/code&gt; needs a careful label: &lt;strong&gt;no visible unfinished CRM activity&lt;/strong&gt;, not “no customer agreement.” A promise could live in a conversation or another system. The useful next action is to check the deal card and correspondence, not reprimand an employee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make coverage part of the data contract
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apidocs.bitrix24.com/api-reference/crm/timeline/activities/activity-base/crm-activity-list.html" rel="noopener noreferrer"&gt;Bitrix24 documents&lt;/a&gt; that &lt;code&gt;crm.activity.list&lt;/code&gt; respects the current user's permissions, paginates results, and returns &lt;code&gt;next&lt;/code&gt; when another page exists. A successful HTTP response doesn't establish that your integration collected the whole selection, much less every record in the company.&lt;/p&gt;

&lt;p&gt;This module keeps coverage per deal. It also reports the collection window and refuses to claim an atomic portal snapshot:&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;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"records_visible_to_webhook_user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"selected_deal_pages_complete"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"all_selected_activity_pages_complete"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"snapshot_is_atomic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"portal_wide_completeness_proven"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;That object is the fixture's actual &lt;code&gt;coverage&lt;/code&gt; output. &lt;code&gt;true&lt;/code&gt; for deal pages and &lt;code&gt;false&lt;/code&gt; for activity pages aren't contradictory: the selection of deals finished, but one deal's activity read didn't.&lt;/p&gt;

&lt;p&gt;Don't discard an entire partial report either. An incomplete read that contains an overdue activity still proves that particular activity was visible. It doesn't prove there were no others. In a separate regression, adding synthetic unfinished activity &lt;code&gt;205&lt;/code&gt; to deal &lt;code&gt;105&lt;/code&gt; produces both &lt;code&gt;activity_coverage_incomplete&lt;/code&gt; and &lt;code&gt;overdue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Positive evidence survives incomplete coverage. An absence claim doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  A later clock is not a newer snapshot
&lt;/h2&gt;

&lt;p&gt;Run the same saved input at two analysis times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-B&lt;/span&gt; bitrix_read_kit.py audit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--input&lt;/span&gt; examples/synthetic-snapshot.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--as-of&lt;/span&gt; 2026-10-10T10:00:00+03:00

python3 &lt;span class="nt"&gt;-B&lt;/span&gt; bitrix_read_kit.py audit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--input&lt;/span&gt; examples/synthetic-snapshot.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--as-of&lt;/span&gt; 2026-10-13T10:00:00+03:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;First analysis&lt;/th&gt;
&lt;th&gt;Later analysis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unique visible unfinished activities&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deals with a scheduled activity&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deals with an overdue activity&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deals with incomplete activity reads&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Activity &lt;code&gt;201&lt;/code&gt; on deal &lt;code&gt;102&lt;/code&gt; has a deadline of 10 October at 12:00 MSK. The first analysis happens before it; the second happens after it. Nothing entered or left the input file.&lt;/p&gt;

&lt;p&gt;Both reports retain &lt;code&gt;captured_at: 2026-10-10T06:00:00Z&lt;/code&gt;. The later report doesn't tell you whether someone completed the activity after capture. It answers “how does this saved evidence classify at this time?”, not “what is happening in the portal now?”&lt;/p&gt;

&lt;p&gt;Keep that difference in the UI. Show capture time beside analysis time. Before a consequential action, obtain an authorized fresh read rather than merely changing &lt;code&gt;--as-of&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is another date trap: Bitrix24's &lt;a href="https://apidocs.bitrix24.com/tutorials/crm/how-to-get-lists/get-activity-list-by-deals.html" rel="noopener noreferrer"&gt;official activity example&lt;/a&gt; uses &lt;code&gt;9999-12-31&lt;/code&gt; for an activity with no deadline. The kit classifies the fixture's activity &lt;code&gt;203&lt;/code&gt; as undated. It doesn't celebrate an appointment scheduled thousands of years ahead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the model a bounded job
&lt;/h2&gt;

&lt;p&gt;Once the checks run, use the model to explain evidence, not manufacture certainty. This prompt works with the included fixture:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Run the two offline audits at the exact times in this article. Don't connect a portal or change the CRM. For each deal, give its ID, visible unfinished activity IDs, coverage, supported conclusion and manual verification step. Keep incomplete reads separate from complete empty selections. Explain why activity 201 changes classification without changing the unfinished count. Distinguish capture time from analysis time. Don't infer lost sales, employee failure or an agreed customer next step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Make the same boundary executable in your integration tests:&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;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_visible_open_activity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activity_coverage_incomplete&lt;/span&gt;&lt;span class="sh"&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;lt;=&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That assertion catches one specific contradiction. It doesn't certify the underlying portal data. Keep provider errors, malformed records and permission checks out of the “empty success” path too.&lt;/p&gt;

&lt;p&gt;For this article, autonomous AI agents reran the pinned source's 95 tests and its clean-install smoke test on macOS arm64 with Python 3.9.6. They passed. No real customer portal supplied these results. Real capture files still contain linkable internal CRM IDs even without names or phone numbers; don't publish them or hand them to an unrelated AI service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the free module, then decide what needs integration
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ilookatshow-gif/bitrix-read-kit/tree/e102c33d20ed5d74591092433e78fb9019e1cc0d?utm_source=devto&amp;amp;utm_medium=technical_article&amp;amp;utm_campaign=bitrix-read-kit&amp;amp;utm_content=source" rel="noopener noreferrer"&gt;Start with the pinned source and fixture&lt;/a&gt;. You can keep the offline checks independent of any agent host. The CLI isn't an MCP server; adding a model doesn't change its evidence limits.&lt;/p&gt;

&lt;p&gt;I'm Dmitry Lukashov, creator of Lookatshow, a marketing-management system for AI agents. If your team needs a CRM review workflow with explicit permissions, freshness rules and a separate read-before-write check, &lt;a href="https://lookatshow.ru/ai-implementation/?utm_source=devto&amp;amp;utm_medium=technical_article&amp;amp;utm_campaign=bitrix-read-kit&amp;amp;utm_content=implementation" rel="noopener noreferrer"&gt;discuss that implementation with me&lt;/a&gt;. The module remains free and doesn't require that service. I share practical notes at &lt;a href="https://t.me/lookatsoul" rel="noopener noreferrer"&gt;@lookatsoul&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Disclosure: this is my project. Autonomous AI agents prepared the article and checked its code and outputs. The linked source, fixture and commands make the technical claims independently testable.&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
