The rule
Preparing an action must not cause its external effect. External effects
occur only through explicit commitment.
foo(x). Sense makes them structurally different.
Declaring an action
reversible/irreversible replaces fn for anything that changes the
outside world — marking the distinction at the declaration site itself,
not bolted on afterward. The distinction is enforced,
not just recorded on .kind: a reversible action must declare a
rollback block (an irreversible one is rejected if it tries to), and
only a reversible action can ever be rolled back —
see Rollback below.
Calling an action never runs its body
This is the part that’s actually enforced, not just documented. Callingsend_mail(...) returns a SenseAction — bound arguments plus a state —
and nothing about that call touches the function body at all:
.commit() executes the body.
Lifecycle
.verify(), .approve(), .commit(), .rollback() — is a
method on the action itself, one consistent calling convention. Skipping
a step is an error, by design:
.verify() and .commit() each independently check policy
.commit() doesn’t trust an earlier
.verify(); it checks again, itself.
requires approval: mandating a human sign-off
requires accepts a capability, the literal word approval, or both
(comma-separated, in either order):
requires approval puts the requirement on the declaration — it can’t
be forgotten by calling code the way a hand-rolled
if ask_human("approve?") == "yes": a.commit() check could be.
<action>.approve() is a separate, explicit method, so the caller still
decides how approval is obtained — commonly
ask_human inside an agent, but it could just
as well be a programmatic check or an external process:
.commit() checks requires approval the same independent,
immediately-before-the-effect way it already re-checks capability — not
trusting anything decided earlier — raising SenseApprovalError if
.approve() was never called. .verify() deliberately does not check
approval, so the natural order is prepare → verify (capability’s fine) →
seek approval → commit, rather than forcing approval before verification.
Audit log
Every prepare/verify/commit/deny/fail event on every action is recorded:Interpreter.audit_log (host-side — a Python list of AuditEntry records
with action_name, kind, event, detail, line, timestamp,
lineage) is always populated, whether or not a Sense program ever reads
it. The audit_log() builtin gives Sense code a simplified read-only view
— an Array<String> of human-readable lines, since there’s no structured
record/map value type yet to hand back the full entry. It’s in-memory
only, scoped to one Interpreter instance — nothing is written anywhere
durable.
lineage records which agent/tool/action call an event happened
inside of, outermost-first (e.g. ["agent:researcher", "tool:outer"]) —
empty for a plain top-level call. describe() only appends a
(via ...) suffix when it’s non-empty, so top-level events read exactly
as above; sense inspect’s Audit log panel uses it to
render nested calls as a call tree instead of a flat list.
Rollback: undoing a committed action
Areversible action must formally define its own undo, by declaring
a rollback block — a reversible action with no actual undo is a claim
the declaration doesn’t back up; irreversible is the honest spelling
for “this can’t be undone,” so it’s rejected at parse time rather than
surfacing later as a runtime surprise. <action>.rollback() runs the
block nested inside the exact scope .commit()’s body ran in — so it
sees not just the original args, but any local variable the commit body
itself computed (an inserted row’s id, the value being overwritten):
The declaration (
rollback:) and the method that runs it
(.rollback()) share one name on purpose. rollback is still a plain
identifier, not a reserved keyword — recognized only by name and a
following : (never () — there’s no bare call left to preserve by
staying one, but a smaller ambiguity remains regardless (a variable
literally named rollback declared right after a reversible action’s
body), rare enough to keep accepting rather than reserve a word used in
exactly one grammar position..rollback() re-checks the action’s capability, same as .commit(). A
denied rollback leaves state at "committed" — nothing changed, so
nothing should look changed. A rollback body that itself raises is
surfaced as an ordinary error, state left at "committed" — there’s no
auto-retry for a partially-failed rollback.
Dry run: preview what would commit without letting it happen
--dry-run uses the same prepare → verify → commit line this whole page
is about: everything through .verify() is already just a check, and the
real effect lives entirely inside .commit()’s (and .rollback()’s) body.
Dry run intercepts exactly there — every capability/approval check still
runs for real, only the body is skipped:
action only — a tool call always runs for real, dry-run or
not. Tool has no prepare/verify/commit staging to
intercept, and the language’s own guidance is that anything world-changing
belongs in an action, not a tool — dry run reuses a boundary the
language already enforces rather than adding a second one.
sense inspect --dry-run applies the identical flag to
the live console, with an always-visible “DRY RUN” badge so a click on
Commit is never ambiguous about whether it’s real.
Continue
Policy
requires and policy: allow/deny — capability-gating an action.Tool
A capability-checked function with no prepare/verify/commit lifecycle —
and the way to close the gap on what an action’s body itself calls.
Pause & Resume
ask_human, the usual way .approve() gets called.Roadmap
What’s still missing from the full Action model: capability cost/risk
dimensions, resource budgets, durable audit logging.

