<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Nikita Yurasov — Notes</title>
  <subtitle>Tools I’m building, projects I’m working on, and lessons from running data and AI systems in production.</subtitle>
  <link rel="alternate" type="text/html" href="https://yurasov.dev/blog/"/>
  <link rel="self" type="application/atom+xml" href="https://yurasov.dev/feed.xml"/>
  <id>https://yurasov.dev/blog/</id>
  <updated>2026-07-30T00:00:00Z</updated>
  <author><name>Nikita Yurasov</name><uri>https://yurasov.dev/</uri></author>
  <entry>
    <title>Your production constraints should be executable</title>
    <link rel="alternate" type="text/html" href="https://yurasov.dev/blog/executable-constraints"/>
    <id>https://yurasov.dev/blog/executable-constraints</id>
    <published>2026-07-30T00:00:00Z</published>
    <updated>2026-07-30T00:00:00Z</updated>
    <summary>AI agents draft grounded dlt sources in minutes. Production still adds constraints — cost, credentials, schedules, schema — and a constraint only counts when it runs as code.</summary>
    <content type="html">&lt;p&gt;An AI-written data pipeline isn’t production-ready until your team’s constraints can run against it as code — not as review comments, not as a README, not as the thing the senior engineer remembers to check.&lt;/p&gt;
&lt;p&gt;The generation half has become fast and routine. dltHub — the company behind the open-source &lt;a href=&quot;https://dlthub.com&quot;&gt;dlt&lt;/a&gt; ingestion library — reported that by January 2026, &lt;a href=&quot;https://dlthub.com/blog/agentic-data-engineering-course&quot;&gt;91% of new dlt pipelines were agent-authored&lt;/a&gt;, and my own ingestion estate has been built that way since November 2025: agents read the API docs, write the &lt;code&gt;rest_api&lt;/code&gt; config, and handle the pagination edge cases I used to budget afternoons for. This is good. I want more of it.&lt;/p&gt;
&lt;p&gt;What didn’t change is the bill. Requests burn finite quota — some are billed per call — and a bad load can surface weeks later as a stale dashboard. Generation got cheap; mistakes stayed priced. That gap is what this post — and the package it introduces — is about.&lt;/p&gt;
&lt;h2&gt;What are agents actually good at?&lt;/h2&gt;
&lt;p&gt;Producing grounded first drafts of declarative sources, quickly. &lt;em&gt;Grounded&lt;/em&gt; means the agent writes against current, retrievable documentation instead of remembered training data: dlt is indexed on &lt;a href=&quot;https://context7.com&quot;&gt;Context7&lt;/a&gt;, a live documentation index coding agents can query, and dltHub publishes machine-readable &lt;code&gt;llms.txt&lt;/code&gt; indexes of its docs. Pointed at those, an assistant writes against APIs that exist.&lt;/p&gt;
&lt;p&gt;dlt’s declarative &lt;code&gt;rest_api&lt;/code&gt; format narrows the task further. Its fields — base URL, authentication, pagination, endpoints — mirror the upstream API’s own documentation, which removes most of the hand-written pagination code where subtle mistakes usually hide. It doesn’t validate those choices, though: a wrong endpoint, auth method, or pagination strategy survives until something executes.&lt;/p&gt;
&lt;p&gt;dltHub’s own &lt;a href=&quot;https://dlthub.com/blog/ai-workbench&quot;&gt;AI Workbench post&lt;/a&gt; puts the remaining problem plainly: “the bottleneck in data engineering has moved. It’s no longer writing the code. It’s trusting the code.” That matches my experience exactly — and trusting the code is a different job than writing it.&lt;/p&gt;
&lt;h2&gt;What does production add that a playground doesn’t?&lt;/h2&gt;
&lt;p&gt;Constraints — decisions about money, credentials, time, and shape that no amount of reading the upstream API’s documentation can settle, because your answers aren’t in it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Money.&lt;/strong&gt; Ingestion is the expensive kind of code to be wrong in: requests consume finite quota, some APIs bill per call, and every load spends warehouse capacity. A retry loop that works is still a bill if it re-extracts a year of history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Credentials.&lt;/strong&gt; Your team has a sanctioned way to provision secrets — a secret manager, a vault, orchestrator-managed variables. Which mechanism is right isn’t the point here; the point is that the choice is a &lt;em&gt;policy&lt;/em&gt;, and a token pasted into source code violates it even when it works.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time.&lt;/strong&gt; A schedule is a contract with the orchestrator, not a comment. “Runs daily” has to be machine-readable somewhere, or it’s tribal knowledge.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shape.&lt;/strong&gt; Which columns land, with which types, with what uniqueness — and what happens on the day the upstream adds a field.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this is AI-specific. Every pipeline I wrote by hand had to answer the same four questions. A repo-grounded agent can &lt;em&gt;apply&lt;/em&gt; your existing policy; what it can’t do is invent it — your quota budget, your sanctioned secret backend, your cadence are not derivable from the API’s docs. What AI changed is one variable: authoring volume. In many codebases — mine included, before this tooling — those answers live in READMEs and reviewers’ heads, and that holds while sources arrive one per quarter. When they arrive in minutes, enforcement-by-memory stops scaling.&lt;/p&gt;
&lt;p&gt;That’s the layer I extracted into &lt;a href=&quot;https://github.com/earlybirdvc/dlt-ops&quot;&gt;dlt-ops&lt;/a&gt;: ingestion policy for dlt projects, encoded as rules that run. It is not an AI product — the same rules gate pipelines written by hand.&lt;/p&gt;
&lt;h2&gt;What does it mean for a constraint to be executable?&lt;/h2&gt;
&lt;p&gt;It has four properties and a tool that runs it: a &lt;strong&gt;scope&lt;/strong&gt; (what it applies to), an &lt;strong&gt;enforcement point&lt;/strong&gt; (when it runs), a &lt;strong&gt;failure behavior&lt;/strong&gt; (what happens on violation), and an &lt;strong&gt;explicit exemption path&lt;/strong&gt; (how you overrule it on purpose, in writing). Without a tool behind them, style guides, review checklists, and tribal knowledge stay advisory.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A constraint that lives in a README is a wish. A constraint that runs is a rule.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;dlt-ops enforces at three distinct moments:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;dlt-ops pipeline validate&lt;/code&gt;&lt;/strong&gt; checks everything statically expressible before pipeline execution: layout, naming, config, schedules, schema contracts, column models, import safety — 21 core rules, plus plugin-owned ones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime preflight&lt;/strong&gt; re-checks the critical subset on every &lt;code&gt;run&lt;/code&gt; and &lt;code&gt;backfill&lt;/code&gt;, because a production scheduler does not run your CLI steps first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pre-load assertions&lt;/strong&gt; inspect extracted data between extract and load — row-count floors and ceilings, required columns, in-batch uniqueness, custom predicates. A violation fails the run by default, or warns; row-level assertions can instead quarantine the offending rows into a &lt;code&gt;_dlt_rejected&lt;/code&gt; table. Assertions gate what lands; they can’t refund extraction cost already spent.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Scheduling shows what the framework looks like end to end. Every source declares a schedule in TOML, from a closed set of tags (&lt;code&gt;@hourly&lt;/code&gt; through &lt;code&gt;@monthly&lt;/code&gt;, plus &lt;code&gt;@manual&lt;/code&gt;) that the Airflow adapter groups into DAGs:&lt;/p&gt;
&lt;pre class=&quot;shiki paper&quot; style=&quot;background-color:#fbf8ef;color:#17140f&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-toml&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#17140F&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#A54F22&quot;&gt;sources&lt;/span&gt;&lt;span style=&quot;color:#17140F&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#A54F22&quot;&gt;github_issues&lt;/span&gt;&lt;span style=&quot;color:#17140F&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#A54F22&quot;&gt;dlt_ops&lt;/span&gt;&lt;span style=&quot;color:#17140F&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#17140F&quot;&gt;schedule = &lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt;&quot;@daily&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Scope: every source. Enforcement point: &lt;code&gt;validate&lt;/code&gt; fails on a missing or invalid value. Failure behavior: the finding names the source and the fix. Exemption: &lt;code&gt;@manual&lt;/code&gt; is the declared escape hatch for sources triggered externally or run ad hoc.&lt;/p&gt;
&lt;p&gt;The same pattern governs the other rules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Secrets are a policy, not a value check.&lt;/strong&gt; Whatever your team sanctions — secret manager, vault, orchestrator variables — plugs in as a secret backend, and &lt;code&gt;validate&lt;/code&gt; proves the sanctioned backend is registered and healthy. Resolution still happens at runtime: no static check can prove the secret’s &lt;em&gt;value&lt;/em&gt; is right, and this one doesn’t claim to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every resource declares its shape.&lt;/strong&gt; &lt;code&gt;columns=&lt;/code&gt; with a Pydantic model is mandatory (a rule), and a resource without a schema contract gets the canonical freeze contract auto-applied — new upstream columns don’t silently land. Evolving contracts are an explicit, justified opt-in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Imports must be side-effect-free.&lt;/strong&gt; One rule of the 21, with the most machinery behind it: &lt;code&gt;validate&lt;/code&gt; imports each source module in a throwaway child process behind a CPython audit hook and reports network and disk activity at import time. It is a detector, not a preventer — the call really fires, once, inside the sandboxed child — and the finding reads like this:&lt;/p&gt;
&lt;pre class=&quot;shiki paper&quot; style=&quot;background-color:#fbf8ef;color:#17140f&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;✗ [github_issues] import_safety: Rule 15: network at import of&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    github_issues.py — socket.connect((&#39;140.82.121.6&#39;, 443))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the class of bug that runs fine on a laptop and then fires on every scheduler heartbeat that parses the file — invisible to linters and type checkers, because it’s valid Python.&lt;/p&gt;
&lt;p&gt;Two more properties hold package-wide. Exemptions are first-class: rules can be disabled project-wide, and most can also be exempted per source with a mandatory written reason — reviewable config, not a shrug. Import safety is the deliberate exception: it takes no per-source exemption, only the project-wide switch.&lt;/p&gt;
&lt;p&gt;And enforcement is honest about where it can act. Five adapter-backed features — assertion quarantine, checkpoints, backfill, remote clean, drift reconcile — refuse at preflight on a destination without a &lt;a href=&quot;https://earlybirdvc.github.io/dlt-ops/reference/destinations/&quot;&gt;&lt;code&gt;DestinationAdapter&lt;/code&gt;&lt;/a&gt;, rather than degrade silently. The runs ledger is the one feature that skips with an INFO line instead, and &lt;code&gt;pipeline status&lt;/code&gt; then reports it as unsupported. DuckDB, Postgres, and BigQuery adapters ship first-party.&lt;/p&gt;
&lt;p&gt;Most of these rules are production scar tissue. The set accreted over nine months of running dlt in production at Earlybird, the Berlin VC fund where I build the data platform — not every rule maps to an incident, but the ones you’d trip over first do. A durable rule begins with a failure worth preventing.&lt;/p&gt;
&lt;h2&gt;What can’t rules catch?&lt;/h2&gt;
&lt;p&gt;Plenty — and an honest checker tells you which checks ran, not that you’re safe.&lt;/p&gt;
&lt;p&gt;Two design choices follow from that. First, coverage is reported, not assumed: when a module fails the sandboxed import, every rule that inspects sources skips it, and &lt;code&gt;validate&lt;/code&gt; says so explicitly — a &lt;code&gt;validation_coverage&lt;/code&gt; error naming what is now &lt;em&gt;unknown&lt;/em&gt;, rather than a shorter, greener report. Second, the rule set doesn’t pretend to enumerate unknown failure modes. There is no recall number for agent-introduced defects here, and I won’t invent one.&lt;/p&gt;
&lt;p&gt;The sharpest example is a policy question wearing a bug costume. A source without an incremental cursor re-extracts the full window on every run — on a billed API, that’s real money. Default validation permits it, because a full refresh is sometimes exactly what you want. Whether it’s an error is itself a policy — so it ships as an opt-in rule: &lt;a href=&quot;https://earlybirdvc.github.io/dlt-ops/configuration/rules/#incremental_cursor_required&quot;&gt;&lt;code&gt;incremental_cursor_required&lt;/code&gt;&lt;/a&gt; (added in 0.1.0) is off by default, and turning it on makes a full refresh something you declare in writing, per source, as an exemption.&lt;/p&gt;
&lt;p&gt;Rules are a floor you raise per incident, not a proof of safety. Import safety detects; it does not prevent. A passing &lt;code&gt;validate&lt;/code&gt; means the configured predicates found nothing — that sentence, and no more.&lt;/p&gt;
&lt;h2&gt;Why this loop fits agents&lt;/h2&gt;
&lt;p&gt;Deterministic findings make the fix loop bounded. The working loop, &lt;a href=&quot;https://earlybirdvc.github.io/dlt-ops/guides/build-sources-with-ai/&quot;&gt;adapted from the docs&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ground the assistant in current dlt and dlt-ops docs (Context7, &lt;code&gt;llms.txt&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Let it write the source.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dlt-ops pipeline validate&lt;/code&gt; — findings are structural and specific: a missing column model, a missing schedule, a socket opened at import.&lt;/li&gt;
&lt;li&gt;Feed the findings back; ask the agent to address them.&lt;/li&gt;
&lt;li&gt;Rerun the same check until it passes. Same code, same config, same environment, same ruleset — same answer.&lt;/li&gt;
&lt;li&gt;Then &lt;code&gt;dlt-ops pipeline run -s &amp;lt;source&amp;gt; -y&lt;/code&gt; against a development destination — local DuckDB — and &lt;code&gt;dlt-ops pipeline status&lt;/code&gt; to read the outcome back.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A passing run proves that one execution against that upstream and that destination succeeded — not production correctness. It’s evidence from one run, recorded where the next engineer can find it — not a guarantee. And the determinism is the point: a second model reviewing the first gives you a second opinion; given the same inputs, a rule gives you the same answer twice.&lt;/p&gt;
&lt;h2&gt;Try it&lt;/h2&gt;
&lt;pre class=&quot;shiki paper&quot; style=&quot;background-color:#fbf8ef;color:#17140f&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#17140F;font-weight:bold&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; &quot;dlt-ops[duckdb]&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#17140F;font-weight:bold&quot;&gt;dlt-ops&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; init&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; demo&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; --example&lt;/span&gt;&lt;span style=&quot;color:#655D50;font-style:italic&quot;&gt;   # scaffold with a runnable example source&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#17140F;font-weight:bold&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; demo&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#17140F;font-weight:bold&quot;&gt;dlt-ops&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; pipeline&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; validate&lt;/span&gt;&lt;span style=&quot;color:#655D50;font-style:italic&quot;&gt;     # pre-execution checks: layout, config, contracts, import safety&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#17140F;font-weight:bold&quot;&gt;dlt-ops&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; pipeline&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; -s&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; demo_events&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; -y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#17140F;font-weight:bold&quot;&gt;dlt-ops&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; pipeline&lt;/span&gt;&lt;span style=&quot;color:#0E7A61&quot;&gt; status&lt;/span&gt;&lt;span style=&quot;color:#655D50;font-style:italic&quot;&gt;       # the run ledger, read back from the destination&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The quickstart runs fully offline: the example source is fixture-backed and lands six typed rows in a local DuckDB file — no credentials, no cloud.&lt;/p&gt;
&lt;p&gt;What it’s not: dlt-ops ships no connectors — your dlt code does all the ingesting (the one place it writes rows itself is diverting assertion-rejected ones to &lt;code&gt;_dlt_rejected&lt;/code&gt;). No model, no agent, no codegen. Not an orchestrator — it declares schedules and generates DAGs for one. And it’s a third-party project, not affiliated with dltHub.&lt;/p&gt;
&lt;p&gt;Docs: &lt;a href=&quot;https://earlybirdvc.github.io/dlt-ops/&quot;&gt;earlybirdvc.github.io/dlt-ops&lt;/a&gt; · source: &lt;a href=&quot;https://github.com/earlybirdvc/dlt-ops&quot;&gt;github.com/earlybirdvc/dlt-ops&lt;/a&gt; · Apache-2.0.&lt;/p&gt;
&lt;p&gt;Next post: I built the same four-source estate twice — once in vanilla dlt, once in dlt-ops — and measured the difference. Vanilla structured fine, my strongest claim didn’t survive the measurement, and the wins were narrower and more specific than I expected.&lt;/p&gt;
</content>
  </entry>
</feed>
