- name
- gx-configure-checkpoint
- description
- Turn a working batch definition and expectation suite into a persisted, re-runnable checkpoint with post-run actions — bind them into a validation definition, group into a named checkpoint, verify it by running it once, and hand off a scheduler-agnostic run snippet. Use when a user wants their checks to survive the session ("make this re-runnable", "schedule this", "notify us when this fails", "update our Data Docs after a run"), or when validation results were printed once and never saved.
- license
- Apache-2.0
# Configure a Great Expectations checkpoint
This skill takes a user from "here is a suite that already validated my data"
to a **persisted, re-runnable checkpoint** — a named, saved grouping of one or
more validation definitions plus the actions that should fire after a run —
verified by actually running it once.
Two objects, each built on work an earlier skill already did:
- A **validation definition** binds one batch definition to one expectation
suite: the specific slice of data, and the specific checks to run against
it.
- A **checkpoint** is the named, persisted grouping of validation definitions
plus the post-run actions to fire — notifications, a Data Docs update — run
as one operation.
Everything here goes through Great Expectations' public configuration API and
produces ordinary project artifacts. Nothing depends on this skill being
present afterwards — the checkpoint this flow builds runs from a plain Python
script outside any agent session, which is the point of it.
## The flow
1. **Preflight** — find out which project (or in-memory session) you are
operating on, and tell the user. See `references/preflight.md`.
2. **Check the precondition** — at least one expectation suite and one
working batch definition must already exist. If either is missing, hand
off and stop.
3. **Bind** — pair batch definitions and suites into validation definitions.
4. **Group and add** — collect validation definitions into a named checkpoint
with post-run actions, chosen from `references/action-catalog.md`,
persisting it explicitly as part of the same step.
5. **Never rely on the run call to persist it for you** — step 4's explicit
add is what makes that safe; see why below.
6. **Verify** by running once, and report per-validation-definition and
per-expectation outcomes.
7. **Confirm persistence**, and offer write-out when the session is in
memory.
8. **Hand off** a run snippet that re-runs the checkpoint from outside this
session — see `references/run-and-schedule.md`.
Steps 2 and 5, and the fetch-first rule below, are where this flow fails
destructively rather than loudly if skipped or reordered. Do not skip step 6
either — a checkpoint that was never run is not a verified result.
## What this skill will not do without being asked
<!-- consent-gate: install -->
- **Install, upgrade, or remove a package, or otherwise modify the
interpreter, virtual environment, environment variables, or shell state.**
Not in response to an error, not preemptively while checking whether
something is present, and not by announcing an intention to install with a
chance to decline attached — none of those is the user asking. When
something is missing, name it and hand over the command; running it is a
separate act that starts only from the user's own instruction, later.
<!-- consent-gate: project -->
- **Create a project directory on the user's disk.** Only after the user has
agreed to write the session out and named where.
<!-- consent-gate: config-file -->
- **Edit an existing project's `great_expectations.yml`.** Only after telling
the user what the edit does and getting a yes.
<!-- consent-gate: saved-file -->
- **Write a file to the user's disk that the user did not ask for and
locate.** Offer it in the conversation first.
## Step 1 — Preflight
Follow `references/preflight.md` in full before touching anything. It
establishes whether you are working against a project on disk or an
in-memory session, tells you what to announce to the user, and covers the
environment problems that silently masquerade as "no project found".
The outcome you carry forward is a `context` object and one fact: whether the
session is file-backed or in memory. Both are fully supported paths, and the
difference matters again at step 7.
## Step 2 — The precondition: a suite and a working batch definition
A checkpoint has nothing to run without both a batch definition to read data
through and an expectation suite to check it with. **There is no acceptable
way to improvise either one here** — do not build a suite from a guess at
what the data looks like, and do not construct a batch definition by hand.
Enumerate what the session already has:
```python
def existing_suites_and_batch_definitions(context):
suite_names = [suite.name for suite in context.suites.all()]
batch_definitions = []
for datasource in context.data_sources.all().values():
for asset in datasource.assets:
for batch_definition in asset.batch_definitions:
batch_definitions.append((datasource.name, asset.name, batch_definition.name))
return suite_names, batch_definitions
```
**If either list comes back empty, stop and route:**
- No batch definition → hand off to `gx-configure-data-source`.
- No expectation suite → hand off to `gx-configure-expectations`.
- Neither exists → lead with the data source, since a suite is built against
a batch and there is nothing to build one against yet.
This is a hard stop, not a suggestion — say plainly what's missing, name the
skill that builds it, and end this flow. Do not carry on and do not
improvise a workaround.
If enumerating what exists, or running the checkpoint in step 6, turns up a
missing driver or client library, report it and hand over the install command
per the standing rule above. Do not install it yourself — not now, before
anything has failed, and not later without the user's own go-ahead.
If more than one suite or batch definition is available, name them and let
the user choose which pairs to bind — guessing which check should run
against which slice of their data is not a decision to make for them.
## Step 3 — Bind: one validation definition per pair
A validation definition needs its suite already persisted in the working
context. **Building one against a suite the context has never seen raises,**
not silently loses work the way an unregistered suite does elsewhere in this
skill family:
```python
from great_expectations.core import ExpectationSuite, ValidationDefinition
unsaved_suite = ExpectationSuite(name="not_saved_yet")
# ValidationDefinition(name="x", data=batch_definition, suite=unsaved_suite)
# ctx.validation_definitions.add(...) on this raises ResourceFreshnessAggregateError:
# "ExpectationSuite 'not_saved_yet' must be added to the DataContext before it
# can be updated. Please call `context.suites.add(<SUITE_OBJECT>)`..."
```
So always fetch the suite (and the batch definition) from the context rather
than closing over an object from earlier in the conversation that might not
be the one actually persisted:
```python
from great_expectations.core import ValidationDefinition
VALIDATION_DEFINITION_NAME = "orders_quality_check"
datasource = context.data_sources.get("warehouse")
asset = datasource.get_asset("orders")
batch_definition = asset.get_batch_definition("by_month")
suite = context.suites.get("orders_quality")
existing = {vd.name for vd in context.validation_definitions.all()}
if VALIDATION_DEFINITION_NAME in existing:
validation_definition = context.validation_definitions.get(VALIDATION_DEFINITION_NAME)
else:
validation_definition = context.validation_definitions.add(
ValidationDefinition(
name=VALIDATION_DEFINITION_NAME, data=batch_definition, suite=suite
)
)
```
Membership is checked through `.all()` rather than a fetch-and-except, the
same reason `references/write-out.md` uses it for these two object types:
`validation_definitions.get()` on a miss raises `DataContextError`, which is
not a `LookupError` — the `except LookupError` idiom from the data-source
skill does not transfer here. Name it in prose, and never import it.
Repeat this once per (batch definition, suite) pair the user wants bound.
Reusing an already-bound pair under the same name is safe — the fetch above
returns it unchanged.
## Step 4 — Group: build the checkpoint, and add it
```python
from great_expectations.checkpoint import Checkpoint
from great_expectations.checkpoint.actions import UpdateDataDocsAction
CHECKPOINT_NAME = "orders_checkpoint"
existing_checkpoints = {c.name for c in context.checkpoints.all()}
if CHECKPOINT_NAME in existing_checkpoints:
checkpoint = context.checkpoints.get(CHECKPOINT_NAME)
else:
checkpoint = context.checkpoints.add(
Checkpoint(
name=CHECKPOINT_NAME,
validation_definitions=[validation_definition],
actions=[UpdateDataDocsAction(name="update_data_docs")],
)
)
```
**This is the explicit add step 5 below is about — the create branch calls
`context.checkpoints.add(...)` directly, so `checkpoint` is a persisted
object on both branches before anything runs.** Get the two branches from
step 3's pattern above if adapting this by hand: fetch on a name that already
exists, build-and-add on one that doesn't. Never construct a bare
`Checkpoint(...)` and hold onto it without one of these two calls — that is
exactly the unpersisted object step 5 exists to warn about.
Ask what should happen after a run — a Slack or Teams message, an email, a
Data Docs rebuild, one of the less common actions — before assuming none is
wanted. `references/action-catalog.md` covers every attachable action: its
fields, which four carry a public-API stability guarantee and which four
don't, and how to word each one's credential requirements.
<!-- consent-gate: config-file -->
**Before attaching a Data Docs update action, check whether the project has a
site to write to.** File-backed projects and in-memory sessions both carry a
working default site; `references/action-catalog.md` covers both, and one
project state that doesn't: `great_expectations.yml` can hold
`data_docs_sites: null`, under which every site-management call silently
does nothing — no error, no site, no sign anything went wrong. The reference
gives the fix, and it edits the user's `great_expectations.yml`, so **ask
before running it.** Finding `data_docs_sites: null` is not, by itself,
permission to change it — state what you found and what the fix involves,
and wait for a yes. If the user declines, name the same fix as the path
forward and build the checkpoint without the action.
## The fetch-first rule: never `add_or_update` an existing checkpoint or validation definition to change it
`context.checkpoints.add_or_update(...)` and
`context.validation_definitions.add_or_update(...)` both persist the
**incoming** object with the stored object's id transplanted onto it — a
whole-object replacement, not a merge. For a checkpoint, the replacement
**cascades**: a fresh validation-definition object passed in as part of the
incoming checkpoint is itself `add_or_update`-ed, and that in turn
`add_or_update`s its own suite. Verified directly, isolating exactly what
that cascade does and does not touch:
| | outcome |
| --- | --- |
| **Always lost** | the checkpoint's validation-definition membership **and its actions** — replaced wholesale by whatever the incoming object listed |
| **Conditionally lost** | a bound suite's *contents* — only when a **fresh** suite object is passed under an existing name; upserting with the suite you fetched from the context first leaves it untouched |
| **Never lost** | validation definitions already in the store — nothing here deletes one; a validation definition dropped from a checkpoint's membership is still fetchable afterward |
Be precise about this when you talk to a user: *"you'll lose this
checkpoint's action list and which validation definitions it groups"* is
accurate; *"your other validation definitions will be deleted"* is not, and
overstating the damage is as much a defect as understating it.
**`get` → mutate → `save` avoids the cascade entirely.** To change an
existing checkpoint's actions, or which validation definitions it groups,
fetch it, mutate the field, and save the same object back — never rebuild it
from scratch and hand it to `add_or_update`:
```python
checkpoint = context.checkpoints.get(CHECKPOINT_NAME)
checkpoint.actions = checkpoint.actions + [UpdateDataDocsAction(name="update_data_docs")]
checkpoint.save()
```
`add_or_update` is not banned outright — it is the right call the one time a
user explicitly wants a checkpoint's whole contents replaced, and only after
you have told them exactly what that discards, per the table above.
## Step 5 — Why step 4 added explicitly, before running
**Never construct a bare `Checkpoint(...)` and hand it straight to `.run()`
without going through the `context.checkpoints.add(...)` /
`context.checkpoints.get(...)` branches in step 4, even though it would
work.** `Checkpoint.run()` auto-persists an unsaved checkpoint whose
validation definitions and suites are themselves already saved — verified
directly: a `Checkpoint` object built and run without ever being added to
the context shows up in `context.checkpoints.all()` immediately afterward,
with no error and no warning that it happened implicitly. That is exactly
why step 4 is written to add explicitly rather than left to be discovered as
unnecessary: skipping it still produces a persisted checkpoint, so nothing
about the flow fails to teach you that the explicit step was needed.
Persistence is something step 4 does on purpose, not a side effect of the
verification run in step 6.
## Step 6 — Verify by one run, and report by identity
```python
result = checkpoint.run(batch_parameters={"dataframe": df})
```
Run this inside the duration-tracked wrapper in `references/robustness.md`,
exactly as the data-source and expectations skills do — a checkpoint can
touch as much data as every validation definition it groups, combined. If the
run fails because a driver or client library is missing, hand over the
install command and stop — do not install it to get the run passing.
`references/run-and-schedule.md` covers what `batch_parameters` actually
does across multiple validation definitions, including a real trap when more
than one of them is dataframe-backed, and why numeric window parameters are
integers on every datasource family.
**Pair each outcome to the validation definition it came from by identity,
never by position:**
```python
for vr_id, vr in result.run_results.items():
print(f"validation definition: batch={vr_id.batch_identifier} suite={vr.suite_name}")
for each in vr.results:
config = each.expectation_config
if each.success:
print(f" PASS {config.type} {config.kwargs}")
elif not each.result:
for _metric_id, info in each.exception_info.items():
print(f" ERROR {config.type} {config.kwargs}: {info['exception_message']}")
else:
print(f" FAIL {config.type} {config.kwargs}: {each.result}")
```
The same infra-vs-data discriminator from the expectations skill applies at
this level too: `success is False` with an **empty** `result` is a metric
error — a broken column or query, not a finding about the data — while a
populated `result` (even one carrying `observed_value: None`, which empty
tables and all-null columns produce routinely) is a real outcome to report as
such. `references/robustness.md` has the full rules for translating either
case into something a user can act on; do not report a metric error as
"your data failed this check."
Report the overall `result.success`, then each validation definition's own
outcome, then each expectation within it — not just a total pass/fail count.
`result.describe()` (the summary method that is `@public_api`) gives the
same information as a formatted JSON string, useful for a short written
recap, but it does not label its entries by name — use the loop above, not
`describe()`, whenever the report needs to say *which* validation definition
an outcome belongs to.
## Step 7 — Confirm persistence, offer write-out
**In a file-backed project, the checkpoint and its validation definitions
are already saved** — the explicit adds in steps 3 and 4 wrote them.
Confirm this concretely: name the checkpoint, name the validation
definitions it groups, and say that a fresh session picks it up with
`context.checkpoints.get("<name>")`.
<!-- consent-gate: project -->
**In an in-memory session, none of it survives the process.** Say this
plainly, and **offer to write the session out** — data sources through
checkpoints — to a real project, per `references/write-out.md`. That
procedure extends past suites to validation definitions and checkpoints,
re-created in the target project only after everything they reference is
already there. Offer it; don't do it unprompted, and don't pick the
location. The offer ends this flow the same way it does in the earlier
skills: write-out is a separate run, starting only after the user has agreed
and named a directory — never in the same program as the steps above.
## Step 8 — Hand off the run snippet
The checkpoint is only re-runnable outside this conversation if there is a
concrete way to re-run it. Give the user a small, self-contained script that
loads the project by an absolute path and re-runs the checkpoint by name —
see `references/run-and-schedule.md` for the exact template, why each of its
choices is deliberate, and the one case (a dataframe-backed validation
definition) where it doesn't apply as written.
<!-- consent-gate: saved-file -->
**Offer to save it to a file at a path the user confirms; never write it
unasked.**
Present it in the conversation first. Wiring it into an actual schedule — a
cron entry, an Airflow DAG, a CI job — is the user's orchestrator's job, not
this skill's; say that plainly if asked, and hand over the snippet as the
piece their scheduler needs to call.
## Worked example
A file-backed project already holds a verified `orders` batch definition,
partitioned monthly, and an `orders_quality` suite built and run earlier by
the other two skills.
```python
import great_expectations as gx
from great_expectations.core import ValidationDefinition
from great_expectations.checkpoint import Checkpoint
from great_expectations.checkpoint.actions import UpdateDataDocsAction
context = gx.get_context(cloud_mode=False) # step 1
# step 2: both preconditions already exist -- proceed
datasource = context.data_sources.get("warehouse")
asset = datasource.get_asset("orders")
batch_definition = asset.get_batch_definition("by_month")
suite = context.suites.get("orders_quality")
VALIDATION_DEFINITION_NAME = "orders_quality_check" # step 3
existing_vds = {vd.name for vd in context.validation_definitions.all()}
if VALIDATION_DEFINITION_NAME in existing_vds:
validation_definition = context.validation_definitions.get(VALIDATION_DEFINITION_NAME)
else:
validation_definition = context.validation_definitions.add(
ValidationDefinition(
name=VALIDATION_DEFINITION_NAME, data=batch_definition, suite=suite
)
)
CHECKPOINT_NAME = "orders_checkpoint" # step 4
existing_checkpoints = {c.name for c in context.checkpoints.all()}
if CHECKPOINT_NAME in existing_checkpoints:
checkpoint = context.checkpoints.get(CHECKPOINT_NAME)
else:
checkpoint = context.checkpoints.add( # step 4: explicit add
Checkpoint(
name=CHECKPOINT_NAME,
validation_definitions=[validation_definition],
actions=[UpdateDataDocsAction(name="update_data_docs")],
)
)
result = checkpoint.run(batch_parameters={"year": 2024, "month": 3}) # step 6
for vr_id, vr in result.run_results.items():
print(f"{vr_id.batch_identifier} / {vr.suite_name}: success={vr.success}")
for each in vr.results:
config = each.expectation_config
status = "PASS" if each.success else ("ERROR" if not each.result else "FAIL")
print(f" {status} {config.type} {config.kwargs}")
```
Against a month where one order has no customer, that prints:
```text
warehouse-orders-year_2024-month_3 / orders_quality: success=False
FAIL expect_column_values_to_not_be_null {'batch_id': 'warehouse-orders-year_2024-month_3', 'column': 'customer'}
```
Report to the user: the checkpoint `orders_checkpoint` is saved, groups one
validation definition (`orders_quality_check`, over the March 2024 window),
ran once, and found one failure — a missing customer on one row. Then move
to step 7's persistence confirmation and step 8's run-snippet handoff.
## Where this flow ends
The persisted, run, reported checkpoint — with its run snippet handed off —
View on GitHub