<?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: CivicDataForge</title>
    <description>The latest articles on DEV Community by CivicDataForge (civicdataforge).</description>
    <link>https://dev.to/civicdataforge</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%2Forganization%2Fprofile_image%2F14479%2F7e76c0da-7bc8-47b3-9e02-22748696e0fa.png</url>
      <title>DEV Community: CivicDataForge</title>
      <link>https://dev.to/civicdataforge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/civicdataforge"/>
    <language>en</language>
    <item>
      <title>What Broke When We Turned Government Open Data Into Agent-Callable Evidence</title>
      <dc:creator>Bryan Williams</dc:creator>
      <pubDate>Mon, 24 Aug 2026 05:25:11 +0000</pubDate>
      <link>https://dev.to/civicdataforge/what-broke-when-we-turned-government-open-data-into-agent-callable-evidence-1m81</link>
      <guid>https://dev.to/civicdataforge/what-broke-when-we-turned-government-open-data-into-agent-callable-evidence-1m81</guid>
      <description>&lt;p&gt;Government open data looks deceptively easy from a distance.&lt;/p&gt;

&lt;p&gt;Find an endpoint. Make a request. Normalize the JSON. Ship an API.&lt;/p&gt;

&lt;p&gt;That is enough for a demo. It is not enough for a production system that has to answer a consequential question next month, after the publisher changes a field, caps a response, moves an endpoint, or returns an empty page that looks exactly like “there are no records.”&lt;/p&gt;

&lt;p&gt;We have been building CivicDataForge around one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What has to exist between an official government publisher and a software system—or an AI agent—before the result deserves to be called evidence?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is the practical answer we have reached so far. It is not a claim that government data can be made perfectly clean. It is an account of the failure modes we hit, the architecture we adopted, and the boundaries we refuse to erase.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A successful HTTP response is not a complete dataset
&lt;/h2&gt;

&lt;p&gt;The first serious failure mode is pagination.&lt;/p&gt;

&lt;p&gt;Government-data platforms often impose a maximum record count. A response can be valid JSON, have a &lt;code&gt;200&lt;/code&gt; status, and still represent only the first page.&lt;/p&gt;

&lt;p&gt;ArcGIS feature services expose controls such as &lt;code&gt;resultOffset&lt;/code&gt; and &lt;code&gt;resultRecordCount&lt;/code&gt;, and may signal that a transfer limit was exceeded. Socrata datasets similarly support paging and query controls. If a collector ignores those contracts, “the source returned 1,000 rows” can quietly become “the jurisdiction has 1,000 records.”&lt;/p&gt;

&lt;p&gt;Those are not the same statement.&lt;/p&gt;

&lt;p&gt;Our collection rule became:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Discover the publisher's page limit.&lt;/li&gt;
&lt;li&gt;Request every page in a stable order.&lt;/li&gt;
&lt;li&gt;Track page count, observed row count, and source-reported totals where available.&lt;/li&gt;
&lt;li&gt;Reject a monitoring baseline when the selected query is capped, partial, or structurally inconsistent.&lt;/li&gt;
&lt;li&gt;Bind the completed scope into a receipt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important product decision is step four. A partial page is useful for exploration, but it cannot prove that a record disappeared between two runs.&lt;/p&gt;

&lt;p&gt;Official references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.arcgis.com/rest/services-reference/enterprise/query-feature-service-layer/" rel="noopener noreferrer"&gt;ArcGIS query feature-service layer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.socrata.com/docs/queries/" rel="noopener noreferrer"&gt;Socrata query documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. “No match” is not a negative legal conclusion
&lt;/h2&gt;

&lt;p&gt;Suppose an address lookup returns no permit record.&lt;/p&gt;

&lt;p&gt;It is tempting to return &lt;code&gt;NOT_PERMITTED&lt;/code&gt; or &lt;code&gt;ILLEGAL&lt;/code&gt;. That is usually stronger than the evidence.&lt;/p&gt;

&lt;p&gt;The selected publisher may not cover the full legal regime. The address may be formatted differently. A local permit may exist in another system. The source may be stale. The query may be incomplete. Or there may simply be no published match.&lt;/p&gt;

&lt;p&gt;We use an explicit, fail-closed decision vocabulary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EVIDENCE_FOUND
NO_PUBLISHED_MATCH
REVIEW_REQUIRED
SOURCE_UNAVAILABLE
SCOPE_INCOMPLETE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This vocabulary is deliberately less dramatic than a binary yes/no result. That is the point.&lt;/p&gt;

&lt;p&gt;An API should not manufacture certainty because a consumer wants a Boolean. If a downstream workflow requires a Boolean, the customer should own the policy that maps evidence states to an action—and should preserve the original state for review.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Provenance has to survive normalization
&lt;/h2&gt;

&lt;p&gt;Normalization is necessary. It is also where evidence can lose its identity.&lt;/p&gt;

&lt;p&gt;We normalize names, addresses, dates, identifiers, and source-specific statuses, but retain enough lineage to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which authority published this record?&lt;/li&gt;
&lt;li&gt;Which endpoint, file, or dataset did it come from?&lt;/li&gt;
&lt;li&gt;What query and scope produced it?&lt;/li&gt;
&lt;li&gt;When was it retrieved?&lt;/li&gt;
&lt;li&gt;What did the publisher call this field?&lt;/li&gt;
&lt;li&gt;Which transformation produced the normalized value?&lt;/li&gt;
&lt;li&gt;Can the record and decision be fingerprinted again?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal evidence envelope looks roughly like this:&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;"source"&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;"publisher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"issuing authority"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://official.example/dataset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"retrieved_at"&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-08-24T12:00:00Z"&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;"scope"&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;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bounded source query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"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="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;"record"&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_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;"publisher-owned-key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"normalized_identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stable-identifier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"record_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:..."&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;"decision"&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;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EVIDENCE_FOUND"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"receipt_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:..."&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 exact fields vary by source. The invariant is that the normalized result does not sever the path back to the official record.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Source health and data freshness are different dimensions
&lt;/h2&gt;

&lt;p&gt;A source can be reachable and stale.&lt;/p&gt;

&lt;p&gt;It can have a stable schema and return incomplete data. It can publish a recent file whose internal dates are old. It can change content without changing a URL. It can change a schema without failing a request.&lt;/p&gt;

&lt;p&gt;So one green check is not enough.&lt;/p&gt;

&lt;p&gt;We monitor independent dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;availability&lt;/li&gt;
&lt;li&gt;schema compatibility&lt;/li&gt;
&lt;li&gt;record-count behavior&lt;/li&gt;
&lt;li&gt;source date integrity&lt;/li&gt;
&lt;li&gt;freshness, when the publisher exposes a defensible freshness signal&lt;/li&gt;
&lt;li&gt;content fingerprint&lt;/li&gt;
&lt;li&gt;contract compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When freshness cannot be measured, we say &lt;code&gt;freshness_not_measured&lt;/code&gt;. We do not let an availability check imply that the data is current.&lt;/p&gt;

&lt;p&gt;This distinction matters because a production monitor should be able to quarantine a bad update instead of replacing a known baseline with an empty or damaged response.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Change monitoring needs replay and correction, not just alerts
&lt;/h2&gt;

&lt;p&gt;“Send a webhook when something changes” is only the visible edge of a reliable change system.&lt;/p&gt;

&lt;p&gt;The harder questions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was the previous snapshot complete?&lt;/li&gt;
&lt;li&gt;Is the new snapshot complete?&lt;/li&gt;
&lt;li&gt;Did the source change, or did the collector fail?&lt;/li&gt;
&lt;li&gt;Can the event be delivered more than once?&lt;/li&gt;
&lt;li&gt;Can the consumer replay it?&lt;/li&gt;
&lt;li&gt;What happens after a human corrects an ambiguous match?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apify webhooks retry failed deliveries. That is helpful, but it means consumers must be idempotent. Our recommended key is the Actor run ID plus event type, with the downstream handler acknowledging quickly and queueing the expensive work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idempotency_key = actor_run_id + ":" + event_type
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We treat a webhook as a notification about an evidence object, not as the evidence object itself. The durable dataset, record hashes, scope, and decision receipt remain the inspectable source of truth.&lt;/p&gt;

&lt;p&gt;Official reference: &lt;a href="https://docs.apify.com/integrations/webhooks/actions" rel="noopener noreferrer"&gt;Apify webhook actions and retry behavior&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. AI agents need selection contracts, not a pile of tools
&lt;/h2&gt;

&lt;p&gt;Publishing an MCP server does not automatically make a tool safe or useful.&lt;/p&gt;

&lt;p&gt;An agent still needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the task the tool supports&lt;/li&gt;
&lt;li&gt;the minimum input&lt;/li&gt;
&lt;li&gt;the preferred identifier&lt;/li&gt;
&lt;li&gt;the source and jurisdiction boundary&lt;/li&gt;
&lt;li&gt;the possible output states&lt;/li&gt;
&lt;li&gt;what the output does &lt;strong&gt;not&lt;/strong&gt; prove&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We expose a stable Streamable HTTP MCP endpoint and machine-readable catalogs, but the more important artifact is the task-to-tool selection contract.&lt;/p&gt;

&lt;p&gt;For example:&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;"task"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verify India company evidence"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preferred_identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Corporate Identification Number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minimum_input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"exact CIN preferred"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"boundary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"research evidence, not automatic KYC or eligibility clearance"&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 transport gets the tool into the agent. The selection contract helps the agent call the right tool and preserve the result's meaning.&lt;/p&gt;

&lt;p&gt;The MCP specification recommends Streamable HTTP for remote servers and defines the transport behavior. Authentication, origin validation, and secret handling still belong to the implementation and client configuration—not to public examples with embedded credentials.&lt;/p&gt;

&lt;p&gt;Official reference: &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/transports" rel="noopener noreferrer"&gt;MCP transports specification&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. We stopped thinking of the product as “a scraper”
&lt;/h2&gt;

&lt;p&gt;The architecture now has six explicit services:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Source feasibility and rights review&lt;/li&gt;
&lt;li&gt;Normalization and identity mapping&lt;/li&gt;
&lt;li&gt;Source health and change monitoring&lt;/li&gt;
&lt;li&gt;Evidence delivery and integration&lt;/li&gt;
&lt;li&gt;Evidence packets and correction paths&lt;/li&gt;
&lt;li&gt;AI-agent tool integration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That framing changes what we build.&lt;/p&gt;

&lt;p&gt;A new source does not enter production because we found an endpoint. It needs a source and rights passport, a completeness test, a normalization contract, health probes, a bounded decision contract, and an acceptance-tested delivery path.&lt;/p&gt;

&lt;p&gt;Likewise, a connector is not “available” because we can imagine it. Public APIs, datasets, schedules, webhooks, file exports, and the remote MCP endpoint are live interfaces. A customer-specific adapter, SFTP route, or platform-native app remains scoped work until its real downstream path passes an acceptance test.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://civicdataforge.pages.dev/service-catalog.json" rel="noopener noreferrer"&gt;service catalog&lt;/a&gt; and &lt;a href="https://civicdataforge.pages.dev/integration-recipes" rel="noopener noreferrer"&gt;integration recipes&lt;/a&gt; publish that boundary in human- and machine-readable form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our mission
&lt;/h2&gt;

&lt;p&gt;CivicDataForge exists to reduce the hours lost between “the government publishes this somewhere” and “a person or system can use it without pretending it says more than it does.”&lt;/p&gt;

&lt;p&gt;Our intention is to build a reusable evidence layer across public records: official-source first, inspectable, correction-friendly, and callable by both software and AI agents.&lt;/p&gt;

&lt;p&gt;Our want is straightforward: make source quality and uncertainty visible enough that developers do not have to choose between a brittle one-off script and an opaque vendor answer.&lt;/p&gt;

&lt;p&gt;We are open to collaboration and grateful for serious support, correction, and feedback. We are not asking for permission to build; we are inviting developers, public servants, data owners, researchers, and AI teams to make the contracts stronger with us.&lt;/p&gt;

&lt;p&gt;What we need most from the developer community is adversarial input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which government source breaks your workflow today?&lt;/li&gt;
&lt;li&gt;Which identifier refuses to match cleanly?&lt;/li&gt;
&lt;li&gt;Which publisher silently caps or reshapes results?&lt;/li&gt;
&lt;li&gt;Which evidence state does your current API flatten into a misleading Boolean?&lt;/li&gt;
&lt;li&gt;Which integration path is missing from the recipes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions are more valuable than generic feature requests because they expose the real contract the system has to survive. If you bring one, we will meet it with respect, evidence, and a willingness to change what the facts prove should change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would do differently
&lt;/h2&gt;

&lt;p&gt;If we started again, we would make three decisions earlier:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define failure and uncertainty states before defining the happy-path schema.&lt;/li&gt;
&lt;li&gt;Prove complete collection before building change monitoring.&lt;/li&gt;
&lt;li&gt;Publish task-selection and evidence-boundary metadata alongside every agent tool.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code that fetches rows is often the smallest part of the system. The defensible work is keeping a source-linked statement truthful after paging, normalization, retries, source drift, and downstream automation.&lt;/p&gt;

&lt;p&gt;That is the layer we are building.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>mcp</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
