Which keyword do I want?
def) or a
plain variable (name = expr) — most code is one of those two.
The rest of this page is for lookup, not learning order: full syntax and
a runnable example per keyword. New to Sense? Start with the
Quickstart or a Tutorial instead.
Declarations and bindings
def
Syntax: "def" NAME "(" params? ")" ["->" Type] : block
Declares a plain function — no capability check, no audit entry, always
runs immediately, never async. Deliberately shaped like Python’s own
def/->, unlike every other declaration in Sense (tool, action,
agent, …), each of which keeps its own keyword-first shape and
returns for a return type instead — a plain function has none of the
capability/safety/agency machinery that motivates those, so it’s the one
concept meant to be exactly what a Python programmer already knows.
Use it when: any ordinary, deterministic computation with no
permission check or audit trail needed. Reach for tool
the moment a call needs a capability check or an audit-log entry.
local
Syntax: local NAME [: Type] = expr
Declares a name scoped to the current block, shadowing any outer name of
the same spelling — plain NAME = expr instead reuses or creates a
binding in the nearest enclosing scope that already has it, or the
current scope if none does.
Use it when: you specifically need a fresh binding that can’t
accidentally write through to an outer variable of the same name (a loop
counter inside a function that happens to share a name with something
outside it, for instance).
set
Syntax: set NAME = expr
Explicit-configuration assignment — used for exactly one thing today:
set delegation = <model>, choosing which Model answers ask()
calls in the current scope and everything nested inside it.
Use it when: you’re wiring up which model a block of code reasons
with. It’s deliberately a different keyword from plain assignment because
“which model answers calls” is runtime configuration, not a program
variable.
import, python, as
Syntax: import ["python"] "module-name" ["as" ALIAS]
Brings a module into scope. import "x" imports a Sense module; adding
python reaches into any installed Python package instead — Sense’s own
interpreter runs in the same Python process, so this needs no FFI.
Use it when: you need something Sense doesn’t have natively — math,
json, os, or any third-party package already installed in the same
environment.
Control flow
if, else
Syntax: if expr: block ["else" (if_stmt | block)]
Ordinary conditional branching, chainable via else if.
while
Syntax: while expr: block
Loops while the condition is truthy, re-checked before every iteration.
for, in
Syntax: for NAME "in" expr: block
Iterates an Array (or the result of range(...)), binding each element
to NAME in turn.
break, continue
Syntax: break / continue
break exits the nearest enclosing while/for immediately; continue
skips straight to that loop’s next iteration.
return, returns
Syntax: return [expr] (in a body) · ("tool"|"action" NAME)(params) ["returns" Type]: (in a tool/action declaration — a plain def uses -> instead, see def)
returns Type on a tool/action declaration is an optional
return-type annotation, checked against what return actually produces.
Bare return with no expression returns nil.
Use it when: a return-type annotation is worth adding whenever a
call’s result matters to its caller — it turns a silent type mismatch
into an immediate SenseRuntimeError at the return site.
Literals and operators
true, false, nil
Syntax: true / false / nil
The Bool literals and the single “no value” literal. nil is what a
missing memory/persistent memory key returns via recall(), and what
an unset optional value looks like generally — there’s no separate
Option/Maybe type.
and, or, not
Syntax: expr "and" expr · expr "or" expr · "not" expr
Word-based logical operators (not &&/||/!) — short-circuiting, same
as most languages.
Safety and capabilities
action, reversible, irreversible
Syntax: ("reversible" | "irreversible") "action" NAME(params) ["requires" cap[, cap...]] : block
Declares a staged effect with a real prepare → verify → commit
lifecycle: calling it only prepares (the body runs, but nothing external
should happen until .commit()); .verify() checks capability and
marks it ready; .commit() re-checks capability independently and
actually performs the effect. reversible requires a rollback block
(see below) — a reversible action with no actual undo is a claim the
declaration doesn’t back up; irreversible forbids one — an operation
that “cannot be reliably undone” shouldn’t offer a rollback that pretends
otherwise.
Use it when: the operation changes something outside the program —
sends an email, charges a card, deletes an account. Use tool
instead for something that isn’t necessarily world-changing (a lookup, a
search).
requires, approval
Syntax: "requires" (capability_path | "approval") [, ...]
Attached to an action or tool declaration. A dotted
capability_path (like email.send) is checked against the caller’s
policy scope; the bare word approval instead mandates a call to
<action>.approve() before .commit() will succeed, raising
SenseApprovalError if skipped — rather than relying on calling code to
remember to add that check by hand.
Use it when: approval is for anything that should never auto-commit
even when policy allows it — a human sign-off step, not a capability
check.
ask_human + approve)
policy, allow, deny
Syntax: "policy" ":" (NEWLINE INDENT ("allow"|"deny") capability_path)+
Declares which capability paths the current scope permits or forbids.
Scopes like set delegation does — a policy written inside a
session/agent only affects that scope, without changing the policy
outside it. Default is allow: a capability with no matching rule anywhere
in the scope chain is permitted.
Use it when: anywhere you’re granting or restricting a tool,
action, or real model provider’s capability string.
rollback (declaration)
Syntax: (required on a reversible action) "rollback" : block
Declares the body <action>.rollback() runs after a .commit(), to undo
the effect — nested inside the exact scope the commit body ran in, so it
sees any local variable that body computed, not just the original args.
Mandatory on reversible action — a reversible action with no
actual undo is a claim the declaration doesn’t back up, so the parser
rejects one that omits it; use irreversible action instead if there
genuinely is no undo, which forbids this block entirely. Shares its name
with the .rollback() method that runs it on purpose (originally spelled
compensate:, a needless asymmetry) — it’s still a plain identifier, not
a reserved keyword, recognized only by name plus a following :.
Use it when: any reversible action — it’s required. Use
irreversible action instead the moment a committed effect has no real,
expressible undo (an email can’t be unsent).
AI-native primitives
model
Syntax: "model" NAME "=" expr
Declares NAME, checked immediately to actually evaluate to a Model
(SenseTypeError otherwise) — and the declaration itself names it:
NAME becomes the model’s own .name, overriding whatever the
expression’s value carried. Required, not optional: naming an
inference(...) result with a plain x = inference(...) (no keyword) is
a SenseSyntaxError, pointing at the fix. This isn’t required everywhere
a Model value flows — only for the direct-call assignment shape.
set delegation = inference(...), a bare inference(...) expression,
and inference(...).name chained straight off the call are all still
legal without it, since none of them names a variable.
Use it when: naming a Model, which — since inference(...) needs
it — is every time. The name matters for more than just tidiness: it’s
what shows up in audit_log() and is readable back via .name.
session
Syntax: "session" NAME : block
A scope boundary, not an autonomous actor — set delegation/policy
written inside a session don’t leak back out once it ends.
Use it when: you want to try a different model or policy for a
self-contained piece of work without affecting the surrounding code.
agent, async (on agent)
Syntax: "async"? "agent" NAME : block
A first-class runtime entity with identity, persistent state, and a
lifecycle — start(agent) runs its body; pause()/resume() can
suspend and continue it mid-body. Plain (sync) agent blocks the caller
until the body finishes or pauses; async agent makes start()/
resume() return immediately instead, running the body on its own
thread — use await(agent) to block for the
result later.
Use it when: plain agent for a sequential unit of autonomous work;
async agent specifically when you want two agents’ bodies — or an
agent’s body and its caller — to make progress at the same time.
tool, async (on tool)
Syntax: "async"? "tool" NAME(params) ["returns" Type] ["requires" cap] [STRING] : block
A capability-checked function with no prepare/verify/commit lifecycle —
calling it runs the body immediately. The optional trailing STRING
description (same source line as the signature) becomes required only
once the tool is handed to ask_with_tools. async tool returns a
Future immediately instead of blocking; await(future) gets the
result.
Use it when: you want typed, permissioned, audited access to
something that isn’t necessarily world-changing. Use action instead
when the call changes the outside world and needs a staged
prepare/verify/commit lifecycle.
memory, persistent
Syntax: "persistent"? "memory" NAME [":" STRING]
A key/value store distinct from an ordinary variable — recall() on a
missing key returns nil rather than raising, unlike a bare undefined
variable. Plain memory is an in-process dict, gone when the run ends.
persistent memory swaps that for a durable, versioned SQLite-backed log
(the optional string becomes a file path instead of a category hint) —
remember/forget always insert a new row rather than overwriting, so
history(key)/as_of(key, version) come for free.
Use it when: plain memory for anything scoped to one run; add
persistent the moment you need the data to survive past this process,
or you want to see how a value changed over time.
Testing
test
Syntax: "test" STRING : block
An isolated, individually-reported assertion block. A failure inside one
test block (via assert(...)) is recorded, not raised — it doesn’t
stop its siblings or the rest of the file.
Use it when: anywhere you’d write a unit test. Run a whole file or
directory with sense test <path>, which discovers test_*.sns/
*_test.sns files and reports PASS/FAIL per block.
Continue
Grammar reference
The complete EBNF these entries are drawn from.
Builtins reference
Every built-in function and member, not just keywords.
Examples
Every keyword above, in complete runnable programs.
Errors reference
What each keyword’s checks raise when they fail.

