Skip to main content

Which keyword do I want?

If none of those fit, you probably want a plain function (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.
Full guide: Functions

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).
Full guide: Variables & Scope

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.
Full guide: Model, Ask & Answer

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.
Full guide: Python Interop · Modules

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.
Full guide: Control Flow · Arrays

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.
Full guide: Functions · Tool

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.
Full guide: Values & Types

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).
Full guide: Actions

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.
Full guide: Actions · Pause & Resume (for 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.
Full guide: Policy

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).
Full guide: Rollback

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.
Full guide: Model, Ask & Answer

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.
Full guide: Sessions

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.
Full guide: Agents · Pause & Resume

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.
Full guide: Tool · Tool-Calling

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.
Full guide: Memory

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.
Full guide: Testing

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.