Watching Lifecycle
WatchOp turns chant lifecycle snapshot and chant lifecycle diff --live into a recurring Temporal workflow. The result is a deployment dashboard that catches drift between change windows — not just 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 six categories (missing / orphan / disappeared / newly observed / drifted / unchanged) |
| Watching | WatchOp({ env, schedule }) | Run the two commands above on a Temporal cron, surface drift as a per-run search attribute |
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 a chant Op and a TemporalSchedule; export both so chant build discovers them:
import { WatchOp } from "@intentius/chant-lexicon-temporal";
const { op, schedule } = WatchOp({ name: "prod-watch", env: "prod", schedule: "*/15 * * * *", // every 15 minutes});
export default op; // the Op — discovered by `chant run prod-watch`export { schedule }; // the TemporalSchedule — deployed by `chant build`That’s the whole declaration. Run chant build and you’ll get:
dist/ops/prod-watch/{workflow,activities,worker}.ts— generated worker code with aSnapshotphase and aDiffphase- A
TemporalScheduleresource namedprod-watch-schedulein the Temporal serializer output, registered viatctl schedule createon the next apply
Submit and start the schedule:
chant run prod-watch # one-shot run, useful for smoke-testing# or apply the schedule and let Temporal trigger it on the cronWhat the generated workflow does
Section titled “What the generated workflow does”The two phases compile down to a workflow that, on each tick:
Snapshot— calls thestateSnapshotactivity, 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— callsstateDiffwithlive: true. The activity returns{ output, exitCode, drifted }. Thedriftedboolean is captured as a workflow search attribute 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.)
The Temporal UI experience
Section titled “The Temporal UI experience”Each generated workflow opens with:
upsertSearchAttributes({ OpName: ["prod-watch"], Watch: ["true"], Env: ["prod"],});After the diff phase resolves, an additional Drift = "true" or Drift = "false" attribute is upserted. So in the Temporal UI you can filter:
| Filter | What it shows |
|---|---|
Watch = "true" | Every watch run across every environment |
Watch = "true" AND Env = "prod" | Just prod’s watch runs |
Watch = "true" AND Drift = "true" | The runs that actually detected drift — the only ones you usually need to look at |
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=1stateDiff’s drifted flag is true (the ARTIFACTS ADDED header matches the drift detector). On the next watch tick the corresponding workflow run will be tagged Drift = "true". Clean up:
helm uninstall demo-driftThe next tick should show Drift = "false" again.
How drift becomes a search attribute
Section titled “How drift becomes a search attribute”outcomeAttribute is a generic Op feature, not a WatchOp-specific one — see Outcome-based search attributes for the full contract. The composite wires it like this:
phase("Diff", [ { kind: "activity", fn: "stateDiff", args: { env: "prod", live: true }, outcomeAttribute: { name: "Drift", from: "drifted" }, },]),The serializer produces:
const __r0 = await stateDiff({ env: "prod", live: true });upsertSearchAttributes({ Drift: [String(__r0?.drifted)] });from is a dot-path into the activity’s return value. With stateDiff’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”stateSnapshot and stateDiff are activities — they pick up the same Temporal 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).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, search attributes, gates, compensation
- Choosing Your Deployment Model — when to opt into Ops at all