Reference

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.

Special forms

Evaluated by the interpreter itself, so they don't follow the usual argument rules.

FormMeaning
(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) · 'xdon'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.

Arithmetic & logic

FunctionNotes
+ - * /variadic arithmetic
modremainder
abs
min · maxlists are searched too, skipping nil and text — (min B1:B9) in a sheet
sum · avgthe 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
notlogical negation

Strings

FunctionNotes
(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)

Lists

FunctionNotes
(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)

Dicts

Literal syntax is {:key value …}. Every operation returns a new dict.

FunctionNotes
(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

Types & conversion

FunctionNotes
(type x)the type as a string
number? · string? · bool? · list? · nil? · symbol? · keyword? · fn?predicates
->string · ->number · ->boolcoercion
(parse s)read EELisp source into a value

Dates

Times are epoch seconds; functions that take a date also accept a "YYYY-MM-DD" string.

FunctionNotes
(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.

Evaluation, JSON, network

FunctionNotes
(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

Prelude

Written in EELisp and loaded at startup — read it as worked examples of the language.

FunctionNotes
id · inc · dec
even? · odd? · zero? · pos? · neg?number predicates
first · second · third · lastlist access by position
take · drop · count
some? · every?does any / do all satisfy a predicate
composecombine two functions
when · unlessmacros with rest parameters

Finding your way around

The language carries its own manual. Two functions, both meant to be typed at a REPL: they print what they find and return nil.

FunctionNotes
(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.

Database

FunctionNotes
(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")).

Agenda

Items

FunctionNotes
(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

Categories & rules

FunctionNotes
(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).

Views, templates, files

FunctionNotes
(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

Sheets

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

Inside a formula

You writeIt means
B1the value in B1 — capitals only, since b1 is an ordinary name
B1:B9the range as a flat list, blanks included as nil
$B$1 · B$1 · $B1pinned whole, by row, by column — a pin survives copying and filling
Rates!A1another 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"))

The functions

FunctionNotes
(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-rowsrows numbered from 1; formulas follow their cells, a deleted reference reads #REF!
(sheet-insert-cols name col n) · sheet-delete-colscolumns 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.

Editor RPC

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.

FunctionNotes
(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.