Skip to content

[FEATURE] Ship agent-skill guidance for configuring data sources and expectations - #12061

Merged
joshua-stauffer merged 18 commits into
developfrom
f/agent-skills/skills-and-installer
Aug 14, 2026
Merged

[FEATURE] Ship agent-skill guidance for configuring data sources and expectations#12061
joshua-stauffer merged 18 commits into
developfrom
f/agent-skills/skills-and-installer

Conversation

@joshua-stauffer

Copy link
Copy Markdown
Collaborator

Great Expectations is increasingly configured and validated through a coding agent rather than by hand-written Python. An agent's general programming knowledge doesn't encode the current, correct sequence of calls for this library — which factory method a given connection type needs, what order a suite has to be registered in before expectations are added to it, how to handle a secret without ever printing it to the conversation. Left to infer this, an agent produces plausible-looking code that's subtly wrong about as often as it's right, and a user who doesn't already know the right pattern has no way to tell the two apart.

This ships guidance that closes that gap, in a place and format an agent's own tooling already knows how to find and read.

What's added

Two "skills" — self-contained guidance directories, each with one entry document plus supporting reference material one directory below it. This is an open, multi-vendor format that several coding-agent platforms already read, so publishing in this shape makes the guidance usable by any agent whose platform speaks it, without a separate integration per agent:

  • gx-configure-data-source — takes an agent from "here is my data" to a verified working batch definition: connect a data source, define an asset, add a batch definition, and prove it by actually reading through it. Its catalog reference derives every configurable type, factory name, and asset/batch-definition surface from the shipped schema index at runtime, rather than a hand-maintained list that would go stale.
  • gx-configure-expectations — turns data quality checks a user describes in words into expectations drawn from the shipped catalog, runs them, and reports results grouped and interpreted correctly (a metric error and a failed expectation are not the same thing, and results don't come back in the order they were added).

Both skills share reference material on session handling: finding or announcing the working context, writing an in-memory session out to a real project, and interpreting slow or failing data operations without either blocking indefinitely or silently giving up.

Why the guidance ships inside the package

The correct calling sequence for a fluent factory method or a suite registration is a function of the exact great_expectations version installed. A separately hosted or generated copy can drift out of sync with any given install the moment either changes independently — silently handing an agent instructions for an API surface that no longer matches what's on disk. Shipping the content in the package ties its version to the code's version by construction, so installing or upgrading the package is what keeps the guidance current.

See docs/adr/0006-ship-agent-skills-with-the-package.md for the full rationale, including why the command surface is python -m great_expectations rather than a new console-script entry point (GX previously shipped and removed one; module invocation needs no packaging-level entry-point wiring and can't collide with anything on PATH).

Command surface

python -m great_expectations skills install [--target agents|claude|all] [--symlink] [--force] [--project-root PATH]
python -m great_expectations skills list [--project-root PATH]

install places the bundled skills into the discovery directories a project's coding agent reads (.agents/skills for Codex/Cursor, .claude/skills for Claude Code/Cursor), by copy by default or by symlink on request. list reports what's bundled and what each target currently holds.

Install model — safety properties

The installer treats the destination as belonging to the user, not the package:

  • A destination that already holds exactly what would be installed is left completely alone.
  • A directory with no ownership manifest was never written by this tool and is refused unconditionally — there's no flag that overwrites it.
  • Once a directory carries a manifest, the tool tells its own untouched copy apart from one the user has since edited, and refuses to replace the latter without an explicit --force.
  • Upgrades stage the full replacement beside the destination and swap it in, so a run that dies partway through never leaves a skill with some files at the new version and some at the old.

This makes the command safe to run again after every upgrade, or simply on the suspicion it was never run at all.

What's tested

210 tests cover: installer safety (each safety property above has a paired test that builds the one situation it exists to prevent and confirms the installer catches it); skill format conformance (frontmatter parses, declared name matches its directory, every reference resolves to a real file, the shared reference documents stay byte-identical across both skills); and the skills' own executable guidance (every code block tagged runnable is executed in order against a real session, including the negative paths the guidance warns about — a query over a missing table, an expectation whose metric errors versus one that genuinely fails, an all-null column, an empty result window).

A CI check (ci/checks/check_installed_agent_skills.py) additionally verifies the installed distribution — built and pip install-ed, not the source tree — actually carries the skills and schema catalogs, and that their installed content hashes match their own ownership manifest. A packaging regression such as a glob that stops matching then fails loudly instead of shipping silently.

Note that this check is added to the import_gx job, and ci.yml runs on pull_request_target — so the workflow definition comes from the base branch. The new step will not execute on this PR; it becomes live once this merges. Reviewers should expect that rather than read its absence as a pass.

Packaging

Adds one package_data glob to setup.py: .agents/skills/**/*.md, scoped to the skills tree rather than a blanket markdown pattern, and matched by file rather than by directory (a package_data pattern only ever selects files, and wheels can't record an empty directory).

No MANIFEST.in change is needed — package_data covers the sdist as well as the wheel, the same property already verified on the catalog PR below.

Stacking

This PR is stacked on #12055 and should merge after it — it depends on the schema catalog indexes that PR ships, which the data-source skill's catalog reference reads at runtime.

The schema generator's expectation list is curated: a generated JSON schema
is only complete if the expectation's class defines the catalog metadata
block (short description, data quality issues, supported data sources).
Five registered expectations define no such block, so emitting schemas for
them would pass off structurally incomplete files as real ones.

Name them explicitly instead, so a completeness check can tell a known,
documented gap apart from an accidental one. Also drop a duplicated entry
from the generator's list; it produced no second file and only invited
confusion about which expectations are covered.
Consumers that want to know what Great Expectations can do currently have
to re-parse every schema file to find out. Emit two generated indexes
alongside the schemas instead, from the same generation step, so they
cannot drift apart.

The datasource index maps each schema file to its exact add_or_update_*
factory method. Most types snake-case cleanly from the class name but six
of twenty-six do not, so the mapping is read from the live type registry
at generation time and frozen into shipped data. Because the method name
is reconstructed from the registered type name, each one is checked
against the real factory surface, so a change to the naming convention
fails here rather than shipping an index pointing at methods that do not
exist.

The expectation index maps each type to its schema file and catalog
metadata, and names the registered expectations that have no curated
metadata block under documented_absent, so a completeness check can tell
a known gap from an accidental one.

Both emitters return their serialized content rather than writing it, so
a drift check can regenerate in memory and compare against the checked-in
file without duplicating the extraction logic it is meant to guard.
A generated file that nothing checks will silently rot. Add assertions
that regenerate each index in memory and compare it byte-for-byte to the
checked-in copy, that every datasource index entry names a factory method
which actually exists, and that every shipped expectation is either
cataloged or explicitly recorded as absent - with the catalog and the
absence list required to be disjoint, so an entry that gains a schema
later cannot linger in both.

The completeness check compares against expectations whose implementation
lives under the great_expectations package rather than the whole registry.
The registry is process-global and test modules register expectations at
import time, so an unfiltered comparison would depend on which other test
modules happened to be collected.

The datasource index guard lives in its own module because the existing
schema test file is skipped on every supported Python, so an assertion
added there could never run.

Hoist the generator's expectation list to a module-level constant so a
drift check can regenerate the index without restating the list, which
would defeat the purpose of the check.
The expectation and datasource schema catalogs describe what this package
can do, and tooling is expected to read them from an installed package
rather than from a source checkout. Until now no JSON shipped at all, so
those catalogs existed only for people working from a clone.

Scope the globs to the two schema trees rather than matching JSON
anywhere under the package, so an unrelated JSON file added elsewhere
later does not silently become part of the distribution.
Resolving the expectation classes by name makes their type opaque to the
checker, so the attribute-defined suppression on the schema call is now
unused and fails the type check under warn-unused-ignores.
Three reference documents an agent consults while operating a session:
finding or announcing the working context, writing an in-memory session
out to a real project, and interpreting slow or failing data operations.

Each procedure was executed against a live context rather than written
from the API surface, which changed several of them:

Neither an unusable GX_HOME nor a discarded cloud configuration produces
any signal at the context entry point - both look identical to having no
project - so the agent checks the environment itself instead of waiting
for an error that never arrives.

Adding a data source under a name that already exists replaces it and
drops every asset on it, so the write-out procedure fetches first and
adds only what is missing, and says plainly what the replacing call would
destroy.

A time budget is a check-in, not a cancellation: the query is already
running on the platform's own compute and keeps costing whether or not
the client waits, so the operation is polled on a worker and the user
decides whether to keep waiting or narrow the data.

Scope reduction leads with a narrower batch definition, because assets
describe what data a project works against and batch definitions are how
it gets sliced. A row limit through a query asset is offered only when no
column supports partitioning, and only as a temporary exploration step.
The entry document carries the guided flow from an empty session to a
batch definition that has been proven to read data. Two behaviors it
encodes are not obvious from the API surface:

Retrieving a batch does not prove a batch definition works. Batch
retrieval is lazy for SQL query assets, so a query over a nonexistent
table returns a batch and only fails when something reads through it.
The flow therefore ends at a head probe, not at retrieval.

Replacing a data source drops every asset on it. Reusing an existing
data source to add one asset would silently destroy the user's other
assets, so the flow fetches first and falls back to the replacing
factory only when nothing is there to reuse. Assets and batch
definitions have no update-in-place factory at all and raise on a
duplicate name, so they reuse or delete-then-recreate.

The catalog reference derives every configurable type, its factory
name, and its asset and batch-definition surface from the shipped
schema index at runtime. The factory naming is irregular in six of
twenty-six cases, and a hand-maintained list would go stale the first
time a type is added.
The entry document turns checks a user describes in words into
expectations drawn from the shipped catalog, runs them, and reports
each result. Four behaviors it encodes are not evident from the API.

A suite must be registered before expectations are added to it.
Expectations added to an unregistered suite validate normally and are
then silently lost, because nothing is wired to a store. The handle
the store returns is the one to build on.

Replacing a suite discards its contents. The upsert factory saves the
incoming suite over the stored one rather than merging, so passing a
fresh empty suite under an existing name zeroes it with no error. The
flow fetches an existing suite and reuses it, and treats replacement
as a separate deliberate act.

Per-expectation results do not come back in the order they were added.
Pairing results to expectations by position appears to work on small
suites and silently mislabels larger ones, so results are paired by
their own configuration.

An empty result means a metric errored rather than data failing, but
only when the expectation also did not succeed: a passing expectation
can carry an empty result too. The two cases are reported differently,
because a broken column is a configuration problem and a failed
expectation is a finding about the data.

Suite names containing dots fan the store out into nested directories,
so they are prohibited.
Agent platforms discover a skill by parsing its frontmatter and
following its relative references, so a skill that drifts out of the
format stops being found rather than failing loudly. These tests hold
the shipped skills to the format: frontmatter parses, the declared
name matches the directory, the description stays within limits, and
every reference resolves to a real file no more than one directory
below the skill root.

The three session references are carried by both skills, because a
skill directory must be self-contained. They are compared byte for
byte, and the failure names the copy direction so drift is a
one-command fix rather than a merge.

Each check ships with a companion test that builds the violation it is
meant to catch and asserts the check reports it. A check that cannot
fail is worse than no check, because it reads as coverage. Discovery
scans for entry documents rather than naming the skills, and asserts
it found some, so an empty scan fails instead of passing quietly.
Guidance that carries code is only as good as the code it carries. The
blocks tagged executable now run in order against a throwaway session
backed by pandas and sqlite, and the run has to reach three end states:
a batch definition that reads data, a validation result carrying an
entry per expectation, and a written-out directory that a fresh
file-backed context loads with its batch definitions and suites intact.
Every other block still has to parse.

The negative paths are pinned too, because each one is a claim the
guidance makes. A query asset over a missing table hands back a batch
and only fails when something reads through it. An expectation whose
metric errored returns an empty payload while one that genuinely failed
returns a populated one -- and since an expectation that passed also
returns an empty payload, both halves of that test are load-bearing.
Empty tables and all-null columns produce results rather than raising,
including the case where a range check passes because nulls count as
missing rather than unexpected. An empty partition window fails at
retrieval, before any probe.

The configure snippet is exercised against a session that already holds
the data source, so it fails if it is ever rewritten to replace rather
than reuse. Replacing a data source drops every asset on it, and a
snippet that teaches the safe form should stop compiling the moment it
stops teaching it.

Result ordering gets the same treatment: results are grouped by column
and expectations whose metric errored are moved to the front, so an
example that pairs results with expectations by position is documenting
a coincidence. The text now describes the mechanism, and the tests hold
it to that.
Agent platforms look for skills in fixed project directories, so the
skills have to be copied out of the package and into the project. The
delicate part is not the copy -- it is that the command has to stay
safe to re-run against a directory that may hold the user's own work.

Each installed skill carries a manifest recording the version that
wrote it and a hash of what was written. That is what makes ownership
decidable. A copy matching its manifest is left untouched, byte for
byte. A copy that still matches its manifest but trails the package is
replaced. A copy that no longer matches has been edited by the user,
so it is refused and left alone until the run explicitly forces it.
A directory with no manifest was never ours and is refused always,
including under force -- ownership is not something a flag can assert.

Ownership is settled before versions are compared, because the reverse
order turns a package having moved on into a licence to overwrite work
the user did by hand.

Writes stage beside the destination and swap in, so an interrupted run
leaves either the previous skill intact or a staging directory the next
run clears -- never a half-written skill. Anything that goes wrong with
one skill is recorded and reported rather than raised, so one
unreadable path costs one destination instead of the whole run.

The package hash covers symlinks by their target rather than by what
they point at, and the walk that feeds it never descends through a
linked directory. Both matter: dereferencing would copy content from
outside the package into the project, and directory recursion through
links varies by Python version, which would make the same tree hash
differently on different interpreters.

Discovery resolves the package through the import system rather than
walking up from a file path, so it works the same for a wheel, an
editable install, and a source checkout. Finding no skills at all is
raised rather than reported: nothing to install is a packaging defect,
and reporting it as a run where nothing went wrong would hide it.
The skills ship inside the package, but agent platforms look for them
in the project. This is the command that bridges the two, invoked as a
module rather than a console script so it needs no entry-point wiring
and cannot collide with anything already on the path.

Install accepts a project root, a target, symlink mode, and a force
flag; list reports what is bundled and what each target currently
holds, naming the version that installed it so a project left behind
by an upgrade is visible before anything is changed. The report is
grouped by outcome and the command succeeds only when nothing failed.

Two details are less obvious than they look.

Failures carry a kind rather than only a message, because the reason a
destination was refused cannot be recovered afterwards by looking at
it. A skill refused for local edits and a skill whose subdirectory
could not be read both still exist and both still hold a valid
manifest. Inferring one from the other means telling a user to hunt
for a stray file when the real problem is a permission bit. Only the
code that made the decision knows, so it now says.

The project root defaults to the working directory lazily rather than
while the arguments are being defined. Reading the working directory
can fail -- it has been deleted or unmounted -- and doing it during
argument definition puts that failure outside the handler that turns
it into a message, so every invocation raised, including the ones
that never needed a directory at all.
The installer decides when to overwrite files in a directory someone
else owns, so these tests check the project on disk rather than the
report the installer returns. A report can say a skill was left
untouched while the bytes underneath it moved; only the filesystem
settles it. Modification times and inodes are compared alongside
contents, because rewriting a file with identical bytes is still a
write.

Every check ships with a companion that breaks the installer in the
one way that check exists to catch, and asserts the check notices. A
test suite that cannot fail is worse than none, because it reads as
coverage.

Building those companions turned up a recurring weakness in the tests
themselves. A helper that sets up more than the behaviour under test
requires quietly removes a dimension from every test that uses it: one
helper always paired an edit with a version change, so nothing could
distinguish a stale skill from an edited one, and deleting that
distinction from the installer left the whole suite green. The same
shape hid three more, including a re-run that asks for the other
install mode and a rebuild that changes content without changing the
version -- the case an editable install lives in.

The helpers now take those dimensions as explicit arguments and
verify them, so a run set up to prove something it does not prove
reports that instead of passing. Each condition the installer tests
before deciding a skill is current or unmodified is now covered by
its own test, one at a time.
The skills are read from the installed package at runtime, so they have to
travel with it rather than exist only in the source tree. Matched by file
rather than by directory, since a package_data pattern only ever selects
files and wheels cannot record an empty directory.
Packaging rules fail silently: a glob that stops matching drops files from
the wheel while every source-tree test stays green. This checks the artifact
users actually install, after installation, so a packaging regression fails
loudly instead of shipping.
Captures why the guidance is version-matched by living inside the
distribution, why the command surface is module invocation rather than a
console script, and why installing copies into a project by default with
an ownership manifest instead of writing over what it did not create.
The rationale is the same; the phrasing no longer dates itself against
whatever the test suite happened to cover at the time it was written.
@netlify

netlify Bot commented Aug 13, 2026

Copy link
Copy Markdown

Deploy Preview for niobium-lead-7998 ready!

Name Link
🔨 Latest commit 8636740
🔍 Latest deploy log https://app.netlify.com/projects/niobium-lead-7998/deploys/6a7f24ed3b7377000876ecbd
😎 Deploy Preview https://deploy-preview-12061.docs.greatexpectations.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@joshua-stauffer
joshua-stauffer marked this pull request as ready for review August 14, 2026 14:24
@joshua-stauffer
joshua-stauffer merged commit 7b258f2 into develop Aug 14, 2026
92 checks passed
@joshua-stauffer
joshua-stauffer deleted the f/agent-skills/skills-and-installer branch August 14, 2026 16:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant