21 special forms and 165 functions — the language core, the prelude written in EELisp itself,
the database, and the agenda. Type (functions) in a REPL and the engine counts them
back for you.
Evaluated by the interpreter itself, so they don't follow the usual argument rules.
| Form | Meaning |
|---|---|
(def name value) | define a global binding |
(defn name (params) body…)(defun name (params) body…) | define a function |
(fn (params) body…)(lambda (params) body…) | an anonymous function |
(let (a 1 b 2) body…) | local bindings, given as one flat list |
(set! name value) | reassign an existing binding |
(if test then else) | two-way branch |
(cond test result …) | flat multi-way branch |
(and a b …) · (or a b …) | short-circuiting logic |
(begin e…) | evaluate in order, return the last |
(for-each var list body…) | loop over a list |
(loop (a 0 b 1) body…) | tail-recursive loop with bindings |
(recur v…) | jump back to the enclosing loop |
(quote x) · 'x | don't evaluate |
(quasiquote x) · ` , ,@ | template, unquote, unquote-splicing |
(defmacro name (params) body…) | define a macro; rest parameters allowed |
A parameter list may end with . rest to collect the remaining arguments.
| Function | Notes |
|---|---|
+ - * / | variadic arithmetic |
mod | remainder |
abs | |
min · max | lists are searched too, skipping nil and text — (min B1:B9) in a sheet |
sum · avg | the same reading of lists; (sum) is 0, avg of nothing is an error |
floor · ceil · round | |
pow · expt | (pow base exp) — expt is the same function |
= · != | equality |
< · > · <= · >= | ordering |
not | logical negation |
| Function | Notes |
|---|---|
(str a b …) | concatenate anything, converting as needed |
(str-len s) | |
(str-upper s) · (str-lower s) | |
(str-trim s) | |
(str-split s sep) | → a list |
(str-join sep parts) | accepts either argument order |
(str-contains s needle) | |
(str-matches s pattern) | regular expression test |
(str-replace s target repl) | replaces every occurrence |
(substr s start end) | |
(str-starts-with s p) · (str-ends-with s p) |
| Function | Notes |
|---|---|
(list a b …) | |
(cons x lst) | prepend |
(head lst) · (car lst) | first element |
(tail lst) · (cdr lst) | everything after the first |
(nth lst i) | 0-based |
(length lst) | |
(append a b) · (reverse lst) | |
(range from to) | end-exclusive |
(map f lst) | |
(filter f lst) | |
(reduce f init lst) | |
(sort-by f lst) | sort by a computed key |
(zip a b) | pairs |
(empty? lst) |
Literal syntax is {:key value …}. Every operation returns a new dict.
| Function | Notes |
|---|---|
(dict :k v …) | construct |
(dict-get d :k) | |
(dict-set d :k v) | → a new dict |
(dict-keys d) · (dict-values d) | |
(dict-has d :k) | |
(dict-merge a b) | later keys win |
| Function | Notes |
|---|---|
(type x) | the type as a string |
number? · string? · bool? · list? · nil? · symbol? · keyword? · fn? | predicates |
->string · ->number · ->bool | coercion |
(parse s) | read EELisp source into a value |
Times are epoch seconds; functions that take a date also accept a "YYYY-MM-DD" string.
| Function | Notes |
|---|---|
(now) | current time |
(today) | midnight today |
(date-format d pattern) | e.g. "EEEE", "yyyy-MM-dd" |
(date-add d n :days) | also :weeks, :months |
(date-diff a b :days) |
Date arithmetic is implemented in the engine — there is no external date dependency.
| Function | Notes |
|---|---|
(print x) · (println x) | captured by the host, not written to a terminal |
(eval form) | evaluate a value as code |
(apply f args) | call with a list of arguments |
(json-parse s) · (json-stringify v) | |
(http-get url) · (http-post url body) | synchronous; the only network access in the language |
Written in EELisp and loaded at startup — read it as worked examples of the language.
| Function | Notes |
|---|---|
id · inc · dec | |
even? · odd? · zero? · pos? · neg? | number predicates |
first · second · third · last | list access by position |
take · drop · count | |
some? · every? | does any / do all satisfy a predicate |
compose | combine two functions |
when · unless | macros with rest parameters |
The language carries its own manual. Two functions, both meant to be typed at a REPL:
they print what they find and return nil.
| Function | Notes |
|---|---|
(functions) | every function, macro and special form in scope, with its call shape |
(functions "str") | only the names containing str; a symbol or keyword works too |
(source map) | one definition — the name is not evaluated, so 'map and "map" also work |
(function-list "str") | the same rows as data — a list of dicts with name, kind, sig and summary |
(source-text "map") | what (source map) prints, returned as a string; the name is evaluated, so it can come from a variable |
For anything written in EELisp, (source f) echoes the definition back as it was
written: the comment block above it, the indentation, the comments inside it. That is why
a ;; note directly above a defn is worth writing — it is the function's
documentation, and nothing has to be repeated anywhere else.
> (source take)
take — function
;; The first n elements. (take 2 '(1 2 3)) → (1 2)
(defn take (n lst)
(if (or (= n 0) (empty? lst))
(list)
(cons (head lst) (take (- n 1) (tail lst)))))
Builtins have no EELisp source to show, so source gives their manual entry
instead — signature, one line of prose, and a worked example.
> (source str-join)
str-join — builtin
(str-join sep parts) → string
Joins a list with a separator. Either argument order is accepted.
(str-join "-" '("a" "b")) → "a-b"
A blank line, or any line with code on it, ends a comment block — so a comment only ever documents the definition directly beneath it. A comment written after a definition on the same line belongs to it too, which is how a file of one-line helpers documents itself.
| Function | Notes |
|---|---|
(deftable name (fields)) | name:type short form, or (name :type … :required … :default … :choices …) |
(insert table {dict}) | → the stored record, with its id |
(query table :where :params :order :asc :desc :limit :select) | → a result-set |
(records rs) | rows of a result-set, as a list |
(update table id {dict}) | |
(delete table id) | soft delete |
(pack table) | physically remove deleted rows |
(count-records table) | |
(database-info) | {:path …} — the file tables and agenda live in, or ":memory:"; :error when a host's file couldn't be opened |
(tables) · (describe table) · (drop-table table) | |
(browse table) | a table view |
(edit table id) | a form view for one record |
(defform name table (field)…) | a named form, computed fields allowed |
(field-get r "name") · (field-set r "name" v) | the key may be a string or a keyword — :name |
(record-id r) |
Field types: string, number, bool, date.
The table is named bare — (query contacts) — or by a call that returns its
name, which is how a program picks a table at run time: (query (dict-get f "table")).
| Function | Notes |
|---|---|
(add-item text :when :priority :category :notes) | |
(add-item-today text) | |
(add "call Bob tomorrow !!") | natural language; parses date, priority, people |
(smart-parse text) | the parse, without storing anything |
(items :search :category :priority :when-before :when-after) | |
(items-on date) · (items-between a b) | |
(item-get id) · (item-set id :k v) · (item-done id) | |
(item->dict item) | an item's fields as a dict — id, text, when, priority, notes, categories … |
(item-count) | |
(every n :weeks) | recurrence — :days, :weeks, :months |
| Function | Notes |
|---|---|
(defcategory work/calls) | hierarchical; parents implied |
(assign id "path") · (unassign id "path") | |
(categories) | |
(defrule name :when … :assign … :action …) | :assign and :action may repeat |
(apply-rules) · (apply-rules id) | runs in a transaction; → number changed |
(auto-categorize true) | apply rules as items arrive |
(rules) · (drop-rule "name") |
Bound inside a rule condition: text, notes, id, categories, props, (get "f"), (has-category "c"), (overdue?), (match n).
| Function | Notes |
|---|---|
(defview name :filter expr :sort-by f :group-by f) · (show name) | filtered, optionally grouped |
(views) · (drop-view "name") | |
(deftemplate name (…)) · (from-template name) | |
(templates) · (drop-template name) | |
(open-agenda "file.db") · (use-agenda name) · (close-agenda name) | isolated databases |
(agendas) | |
(export-agenda "f.json") · (import-agenda "f.json") | import runs in a transaction |
A grid whose formulas are EELisp, each sheet a SQLite file of its own. A sheet is named
"Budget", "money/Budget" or by absolute path — relative to
(current-dir), with .eesheet implied. Cells are "C3",
areas "A1:C9".
A cell holds what was typed: "1200" is a number, "rent" text,
"'42" the text 42, and "=(sum B1:B2)" a formula — one expression after
=, in which C3 and $C$3 are that cell's value and
A1:C9 a flat list of values, nil for blanks. Each cell keeps its last
result, so opening a sheet runs no formula; changing a cell recalculates what depends on it.
A number written the way people write one is read as one, and asks for the format it was wearing:
50% is 0.5 shown as a percentage, $1,200 and R$ 1.200,50 are
money, 1 200,50 is twelve hundred and a half. Whichever of . or
, comes last is the decimal point; a lone comma before three digits is separating
thousands, so 1,200 is a thousand two hundred and 1,5 is one and a half.
A format already on the cell wins. 2026-09-16 is a date — the language's own
spelling, which sorts and does arithmetic; 16/09/2026 stays text, because a sheet
guessing which half is the month is worse than leaving it alone.
Rates!A1 and Rates!A1:A9 read another sheet, named beside the one
reading it — money/Q3!A1 from a sheet in the same folder as money/.
Writing a sheet redoes the open sheets that read it, once each, so two sheets naming each other
settle rather than bounce. A sheet that wasn't open when it changed is picked up by ↻.
(sheet-new "Budget")
(sheet-set "Budget" "A1"
'(("rent" "1200") ("food" "450") ("total" "=(sum B1:B2)")))
(sheet-get "Budget" "B3") ; → 1650
(sheet-rows "Budget" "A1:B2") ; → (("rent" 1200) ("food" 450))
(sheet-set "Budget" "B2" "500")
(sheet-get "Budget" "B3") ; → 1700
| You write | It means |
|---|---|
B1 | the value in B1 — capitals only, since b1 is an ordinary name |
B1:B9 | the range as a flat list, blanks included as nil |
$B$1 · B$1 · $B1 | pinned whole, by row, by column — a pin survives copying and filling |
Rates!A1 | another sheet's cell, that sheet named beside this one |
#REF! | what a reference becomes when its cell is deleted |
There are no separate spreadsheet functions: a formula is an expression, so everything on this
page is available in one. The ones reached for most are
sum, avg, min, max and length
over a range; round and if; str and
str-join for text; map, filter and reduce
when a range needs working over; date-add, date-diff and
date-format for dates; and count-records or query to
reach the database.
=(sum B1:B9)
=(round (/ B1 $E$1) 2) ; a share of the total; the pin
; survives being filled down
=(sum (filter (fn (x) (> x 400)) B1:B3))
=(date-format (today) "yyyy-MM-dd") ; (today) is an instant
=(count-records contacts :where "city = ?" :params '("Lisbon"))
| Function | Notes |
|---|---|
(sheet-new name) | an empty sheet file → its path; an error if it exists |
(sheet-open name) | what a host draws: a dict of :path, :version, :cells — each (row col input value error fmt), from 0 — and :widths |
(sheet-close name) | let go of the file — before renaming or deleting it |
(sheet-get name cell) | one cell's value; a failed formula is an error |
(sheet-rows name area) | an area's values as a list of rows |
(sheet-set name cell input) | typed input — a list of rows types a block, as a paste would → the changed cells |
(sheet-put name cell data) | values stay values; a result set writes a header row and its records |
(sheet-copy name area) | what was typed, as rows of text |
(sheet-paste name cell rows from) | types them back; from — where they were copied — moves each formula by the distance travelled |
(sheet-fill name source target) | repeats the source over the target, each copy's references shifted by where it lands |
(sheet-recalc name) | rerun every formula — those that read the database, other sheets or the clock aren't tracked |
(sheet-format name area fmt) | merge a format like {:num "currency" :dp 2} into an area; nil clears; rows of formats set cells exactly. The keys are the host's: :num :dp :cur :date :bold :italic :align :wrap :bg :fg :border |
(sheet-col-width name col width) · (sheet-row-height name row height) | nil for the default; rows are numbered from 1 |
(sheet-insert-rows name row n) · sheet-delete-rows | rows numbered from 1; formulas follow their cells, a deleted reference reads #REF! |
(sheet-insert-cols name col n) · sheet-delete-cols | columns by letter |
(sheet-version name) | moves on every write |
A formula may read sheets — Rates!A1, or (sheet-get "Rates" "A1") for a
name it works out as it runs — but not change them.
Installed by the host, so these exist when the engine runs inside an editor and not on the bare command line. They let a script read and change the document it is running in, and read the workspace around it — never a path outside it.
| Function | Notes |
|---|---|
(buffer-text) | the whole document |
(current-file) | path of the open note |
(current-dir) | the workspace root — the folder being edited |
(notes [folder]) | every note (.md) in the workspace, or under a folder, sorted |
(read-note path) | a file's text as saved, or nil |
(dir-list [folder] [:hidden true]) | one folder's entries, folders first, each a dict: :name :path :dir :size :modified (epoch seconds, like now) :ext |
(file-info path) | one entry as dir-list gives it, or nil |
(cursor-pos) · (set-cursor n) | |
(selection) | the selected text |
(insert-at pos text) | |
(replace-range from to text) |
In EEditor, keybindings use a higher-level set of commands
on top of these — see
the keybinding reference — and a
form's handlers use ui-get, ui-set and friends, which are ordinary
EELisp functions the app defines; they are in
the forms chapter.