Plan, apply and drift
cicada borrows the infrastructure-as-code lifecycle and adapts it to calendars, whose APIs are not transactional. This page is the mental model: what the three states are, why apply is safe by construction, and what the contracts guarantee.
Three states
- Desired — what your YAML declares.
- Recorded —
.cicada/state.yaml: the last reconciled state, keyed by your stable local ids, plus the apply journal. - Live — what the provider actually has right now.
The stable local id (id: standup) is the anchor. Export-only calendar tools fail precisely here: without stable ids mapped to remote ids, a re-run can only create duplicates. Recorded state is what lets cicada update and delete, not just create.
- Plan diffs desired vs recorded (what you changed) and recorded vs live (what changed out-of-band) into ordered operations:
create,update,delete,replace,noop. - Apply executes the plan and persists new recorded state.
- Drift is recorded ≠ live — the remote edit a colleague made in the provider UI.
The managed boundary
The managed set is exactly recorded state. Consequences:
deleteops are generated only for local ids recorded but absent from your YAML.- Live events unknown to recorded state are never modified or deleted — a human's lunch on a managed calendar is untouchable.
- Two cicada sources (two repos, an agent and a human) can share one provider calendar, each reconciling only its own recorded slice.
Every event cicada writes carries provider-side tags — on Google, extendedProperties.private["cicada:id"] plus cicada:managed and a cicada:state content hash. Tags, not guesses, delimit the boundary: they enable state rebuild, adoption reporting, and safe resume.
Apply is sequenced, idempotent, resumable
Calendar APIs have no transactions, so cicada is honest about partiality instead of faking atomicity:
- Ops run in a fixed order —
delete,replace,create,update,noop— minimising conflicts. - Each op carries an idempotency key (
calendarId | localId | opKind | contentHash); providers that honour request ids use it. - State is written per op, as a journal inside the versioned state document. A crash mid-apply leaves a valid, resumable file.
apply --resumeskips journaled ops. And because the failure window is "provider write landed, journal write crashed", resume also reconciles bycicada:idtag — acreate/replacethat finds its tag already live is journalledskippedAlreadyApplied, not duplicated. This check runs on every apply, which is also how a lost state file gets rebuilt.- A failed op is journalled
failedwith its error kind and does not abort the run — the report carries every outcome.
Apply is convergent, not atomic. There is no rollback; there is resume.
Conflict and notification policy
Apply performs an implicit refresh of the managed set first. If live diverged from recorded for an event the plan touches:
- Default: the op fails with a
conflicterror; state is untouched for it.cicada refresh+ re-plan resolves. --force: desired wins; the live edit is overwritten (logged). The recorded etag is dropped so the write lands unconditionally.
Where the provider supports conditional writes (Google: etag → If-Match), replace/update carry the recorded etag — optimistic concurrency where available, conflict checking otherwise.
Providers email attendees on writes, so sendUpdates is a first-class knob (spec.apply.sendUpdates, --notify, the plan.apply parameter) defaulting to externalOnly — drift repairs shouldn't spam the team.
Versioned public contracts
The state file (cicada/state-v1) and plan output (cicada/plan-v1 via plan --out) are versioned documents that CI and agents can depend on; migrations are explicit. Plan findings carry severity — warning informs, error blocks apply (capability gaps surface before anything writes).
Why this shape
The alternatives were rejected for concrete reasons: Terraform couples the core to a plugin RPC model; export-only tools can't update, delete, or detect drift; heuristic stateless matching (summary + time) is fragile for exactly the updates and deletes that matter. Stable ids plus recorded state is the only sound foundation for idempotent apply — and the journal is what makes an untransactional world resumable anyway.