Watching Lifecycle
WatchOp turns chant lifecycle snapshot and chant lifecycle diff --live into an Op with a cadence on it. The result is a drift signal that fires between change windows rather than only at the next deploy.
This page is the tutorial home for that pattern. For the underlying CLI commands, see chant lifecycle; for the Op execution model, see Ops.
Three layers, one composite
Section titled “Three layers, one composite”Drift detection in chant is built from three pieces that compose:
| Layer | Command | What it does |
|---|---|---|
| Snapshot | chant lifecycle snapshot <env> | Query each lexicon’s describeResources() / listArtifacts(), write the result to the chant/lifecycle orphan branch |
| Live diff | chant lifecycle diff <env> --live | Query the cloud right now, compare against the current build and the last snapshot, group results into seven categories (missing / orphan / disappeared / newly observed / drifted / unchanged / unobserved) |
| Watching | WatchOp({ env, schedule }) | Run the two commands above on a cron, and record drift as an outcome on each run |
Snapshots without diffs are forensic record. Diffs without scheduling are ad-hoc. The composite is what makes drift a continuous signal rather than a manual chore.
Declaring a WatchOp
Section titled “Declaring a WatchOp”Save anywhere in your project as a *.op.ts file. The composite returns { op }, and the cadence rides on the Op itself:
import { WatchOp } from "@intentius/chant/op";
const { op } = WatchOp({ name: "prod-watch", env: "prod", schedule: "*/15 * * * *", // every 15 minutes});
export default op; // discovered by `chant run prod-watch`That’s the whole declaration. chant build writes dist/ops/prod-watch/op.json, the engine-neutral restatement of the two phases, and schedule lands on it as { cron: "*/15 * * * *", overlap: "skip" }.
Run it by hand whenever you want:
chant run prod-watch # one tick, right nowThree readers turn the cadence into something that fires on its own. The github generator (generateOpsPipeline) synthesizes a cron-triggered workflow that runs chant run prod-watch in CI. chant operator ticks it from a loop on a machine you already have. A fountain Steward that lists the op turns the cron into a Schedule on its teammate, in-thread, so a fire that lands mid-turn is dropped rather than opening a second machine beside the first.
What a tick does
Section titled “What a tick does”The two phases run in order on each tick:
Snapshot— calls thelifecycleSnapshotactivity, which shells out tochant lifecycle snapshot prod. The result is committed to thechant/lifecycleorphan branch. (Concurrent runs are protected by--force-with-lease— see Concurrent snapshots.)Diff— callslifecycleDiffwithlive: true. The activity returns{ output, exitCode, drifted }. Thedriftedboolean is captured onto the run record viaoutcomeAttribute: { name: "Drift", from: "drifted" }.
Set live: false on the composite to use digest-only diff (faster, no cloud queries) for environments without describeResources() coverage. With live: true (the default) you get external-mutation detection. (See the Runtime observation coverage matrix for which lexicons report what.)
receipts: [myReceipt, ...] (typed EffectReceipt references) adds a read-only Receipts phase that reads each effect receipt through the receipt store and reports absent or differing receipts as findings, recorded as a StaleReceipts outcome — it never runs an effect and never writes a receipt.
Reading the ticks back
Section titled “Reading the ticks back”WatchOp sets labels on the declaration:
labels: { OpName: "prod-watch", Watch: "true", Env: "prod" }Those labels ride onto every run record the Op writes, and the Drift outcome lands beside them once the Diff phase settles. The record goes to prod/runs__prod-watch.jsonl on the chant/lifecycle branch, because Env is prod.
chant run log prod-watch # every run, newest firstchant run status prod-watch # the latest onechant operator log --env prod # every tick in the environment, one timelineNothing has to be running to read any of that: the ledger is a branch in your own repo. When the op is hosted, its turn is also a message on the steward’s thread, so the same tick is legible in fountain’s UI as one command somebody could have typed. See The Steward for which store owns what.
Smoke-testing the drift signal
Section titled “Smoke-testing the drift signal”To prove the pipeline end-to-end, introduce a real out-of-band mutation and watch it surface.
The simplest path is the Helm lexicon’s listArtifacts(), which lists Helm releases visible to your current kubeconfig context. Install something chant doesn’t know about:
helm install demo-drift bitnami/nginx --set image.tag=hotfixchant lifecycle diff prod --liveOutput (relevant section):
ARTIFACTS ADDED (1) helm:demo-drift chart=nginx rev=1lifecycleDiff’s drifted flag is true (the ARTIFACTS ADDED header matches the drift detector). The next tick’s run record carries Drift = "true". Clean up:
helm uninstall demo-driftThe next tick should show Drift = "false" again.
How drift becomes an outcome
Section titled “How drift becomes an outcome”outcomeAttribute is a generic Op feature, not a WatchOp-specific one — see Outcome attributes for the full contract. The composite wires it like this:
phase("Diff", [ { kind: "activity", fn: "lifecycleDiff", args: { env: "prod", live: true }, outcomeAttribute: { name: "Drift", from: "drifted" }, },]),The run reads drifted off the activity’s return value and folds it into the record’s outcomes as Drift, so chant run prod-watch --json prints it and every later reader of the ledger sees the same field.
from is a dot-path into the activity’s return value. With lifecycleDiff’s shape { output, exitCode, drifted }, you could just as easily capture the exit code (from: "exitCode") or the raw output (omit from to stringify the whole result). The composite picks drifted because it’s the field most worth filtering on.
Choosing a schedule
Section titled “Choosing a schedule”Cron expressions follow standard 5-field syntax. A few starting points:
| Cadence | Cron | When to use |
|---|---|---|
| Every 15 min | */15 * * * * | Fast feedback on prod, low API load — the default we recommend |
| Hourly | 0 * * * * | Lower-traffic envs, expensive describeResources() |
| Nightly | 0 3 * * * | Compliance-style audit, no expectation of human follow-up between ticks |
Bias toward less frequent. Each tick re-queries every covered lexicon’s API; rate limits and per-call cost are real concerns at minute-level frequencies for large estates.
Profile selection
Section titled “Profile selection”lifecycleSnapshot and lifecycleDiff are activities — they pick up the same retry/timeout profiles as any other Op step. Both default to fastIdempotent (5 min timeout, 3 retries) which is right for most environments.
If your snapshot regularly takes longer than five minutes (large multi-region estates, slow kubectl against many resources), declare the activity with profile: "longInfra" directly instead of using the composite — WatchOp is a thin wrapper, you can always emit the equivalent Op({ phases: [...] }) by hand.
What to do when drift fires
Section titled “What to do when drift fires”The signal tells you something changed. The output tells you what. The next move depends on which category fired:
MISSING— declared in source, gone from the cloud. Usually a deletion or stack-rollback. Re-apply your stack or update source if the deletion was intentional.ORPHAN— present in the cloud, not in source. Either a teammate created it manually (move it into source) or it’s untracked tooling output (usechant importto bring it in, or scope it out of the env).DRIFTED— observed in both snapshots; attributes changed. The deltas are listed inline. Decide whether to re-apply (snap back to declared) or update source (accept the new shape). A property-level drift that fires every tick because something else legitimately operates that field, an autoscaler onreplicas, say, or a controller stamping an annotation on every sync, is worth declaringheldElsewhere()rather than re-triaging on every run. It stops appearing as drift, and shows up in the plan’s HELD section with who holds it and why.DISAPPEARED/NEWLY OBSERVED— historical context across snapshots. Useful for incident timelines, less actionable per-tick.ARTIFACTS ADDED/REMOVED/CHANGED— context-keyed (Helm releases, Docker containers). Same triage as the resource categories.
Drift remediation is intentionally not a chant primitive — what to do about a drifted security group is a domain decision that doesn’t generalize. The WatchOp pattern stops at “tell me when it happened.”
See also
Section titled “See also”- Drift Detection — the conceptual model behind snapshots and the diff categories
chant lifecycle— full reference for snapshot/diff CLI- Ops — Op execution model, labels, gates, compensation
- Choosing Your Deployment Model — when to opt into Ops at all