Learn EELisp by building real projects. From your first expression to a compound interest calculator, expense tracker, and Lotus Agenda-inspired personal organizer.
Open the REPL in EEditor with ⌘⇧L. You'll see a prompt. Type an expression and press Enter.
In EELisp, everything is an expression wrapped in parentheses. The first item is the function, the rest are arguments:
;; Arithmetic — the operator comes first (+ 1 2) ; → 3 (+ 1 2 3 4) ; → 10 (variadic!) (* 6 7) ; → 42 (/ 100 3) ; → 33.333... ;; Nesting — inner expressions evaluate first (+ (* 3 4) (- 10 5)) ; → 17 (12 + 5) ;; Strings (str "Hello, " "world!") ; → "Hello, world!" (str-upper "hello") ; → "HELLO" ;; Math functions (pow 2 10) ; → 1024 (round 3.14159 2) ; → 3.14 (round to N decimal places) (abs -42) ; → 42 ;; Comparisons return true or false (> 10 5) ; → true (= 1 1) ; → true
(+1 2) works the same as (+ 1 2).
Use def to bind a name to a value:
(def name "Alice") (def age 30) (def pi 3.14159) (str "Hi, " name "! You are " age " years old.") ; → "Hi, Alice! You are 30 years old."
Use defn to create named functions:
;; A function that doubles a number (defn double (x) (* x 2)) (double 21) ; → 42 ;; A function with multiple parameters (defn greet (name greeting) (str greeting ", " name "!")) (greet "Bob" "Good morning") ; → "Good morning, Bob!" ;; Conditional logic with if (defn abs-val (n) (if (< n 0) (- 0 n) n)) (abs-val -5) ; → 5 (abs-val 3) ; → 3
Use fn for one-off functions without a name:
(map (fn (x) (* x x)) '(1 2 3 4 5)) ; → (1 4 9 16 25)
Lists are the bread and butter of Lisp. They hold ordered sequences of values:
;; Creating lists (def numbers (list 1 2 3 4 5)) (def fruits '("apple" "banana" "cherry")) ;; Accessing elements (first numbers) ; → 1 (second numbers) ; → 2 (last numbers) ; → 5 (nth fruits 1) ; → "banana" (length numbers) ; → 5 ;; Building lists (cons 0 numbers) ; → (0 1 2 3 4 5) (append numbers '(6 7)) ; → (1 2 3 4 5 6 7) (range 1 11) ; → (1 2 3 4 5 6 7 8 9 10)
The three most powerful list operations:
;; map — apply a function to every element (map double (range 1 6)) ; → (2 4 6 8 10) ;; filter — keep elements that match a condition (filter even? (range 1 11)) ; → (2 4 6 8 10) ;; reduce — combine all elements into one value (reduce + 0 (range 1 101)) ; → 5050 (sum of 1 to 100) ;; Compose them: sum of squares of even numbers from 1-10 (reduce + 0 (map (fn (x) (* x x)) (filter even? (range 1 11)))) ; → 220 (4 + 16 + 36 + 64 + 100)
Dictionaries store key-value pairs using keywords (prefixed with :):
;; Create a dictionary (def person {:name "Alice" :age 30 :city "NYC"}) ;; Access values (dict-get person :name) ; → "Alice" (dict-get person :age) ; → 30 ;; Update (returns a new dict) (def older (dict-set person :age 31)) (dict-get older :age) ; → 31 ;; Inspect (dict-keys person) ; → (:name :age :city) (dict-values person) ; → ("Alice" 30 "NYC") (dict-has person :email) ; → false ;; Merge two dicts (second wins on conflicts) (dict-merge person {:email "alice@example.com" :age 31}) ; → {:name "Alice" :age 31 :city "NYC" :email "alice@example.com"}
Let's build a real financial calculator that computes yearly compound interest. This is the formula:
A = P × (1 + r/n)^(n×t)
Where P = principal, r = annual rate, n = compounds per year, t = years.
;; Compound interest formula using pow ;; A = P * (1 + r/n)^(n*t) (defn compound-interest (principal rate compounds-per-year years) (let ((r (/ rate 100)) (n compounds-per-year)) (* principal (pow (+ 1 (/ r n)) (* n years))))) ;; Test: $1000 at 5% annual, compounded monthly, for 10 years (round (compound-interest 1000 5 12 10) 2) ; → 1647.01 (you earned $647.01 in interest!)
;; Show balance at the end of each year (defn yearly-breakdown (principal rate compounds-per-year years) (println (str "Investment: $" principal " at " rate "%")) (println (str "Compounding: " compounds-per-year "x/year for " years " years")) (println "---") (for-each year (range 1 (+ years 1)) (let ((balance (compound-interest principal rate compounds-per-year year)) (interest (- balance principal))) (println (str "Year " year ": $" (round balance 2) " (+" (round interest 2) " interest)"))))) (yearly-breakdown 10000 7 12 5)
;; Compare multiple interest rates side by side (defn compare-rates (principal years . rates) (println (str "Comparing $" principal " over " years " years:")) (for-each rate rates (let ((final (compound-interest principal rate 12 years)) (gain (round (- final principal) 2))) (println (str " " rate "% → $" (round final 2) " (+" gain ")"))))) (compare-rates 10000 20 3 5 7 10)
. rates) to accept a variable number of arguments. This is perfect for comparing multiple values.
EELisp has a built-in SQLite database. Let's use it to manage contacts — just like a mini dBASE.
;; Define a contacts table with typed fields (deftable contacts (name:string email:string phone:string city:string age:number)) ;; The table is created. You can verify: (describe contacts) (tables) ; → ("contacts")
;; Insert contacts — each returns the new record with an auto-ID (insert contacts {:name "Alice" :email "alice@mail.com" :phone "555-0101" :city "New York" :age 30}) (insert contacts {:name "Bob" :email "bob@mail.com" :phone "555-0102" :city "San Francisco" :age 25}) (insert contacts {:name "Carol" :email "carol@mail.com" :phone "555-0103" :city "New York" :age 35}) (insert contacts {:name "Dan" :email "dan@mail.com" :phone "555-0104" :city "Chicago" :age 28}) (insert contacts {:name "Eve" :email "eve@mail.com" :phone "555-0105" :city "San Francisco" :age 32})
;; See all contacts (displayed as a formatted table) (query contacts) ;; Find contacts in New York (query contacts :where "city = ?" :params (list "New York")) ;; Find contacts over 30, sorted by name (query contacts :where "age > ?" :params (list 30) :order "name") ;; Count contacts (count-records contacts) ; → 5 ;; Select only specific columns (query contacts :select "name, city" :order "city")
;; Update Bob's city (record ID 2) (update contacts 2 {:city "Los Angeles"}) ;; Verify the change (query contacts :where "name = ?" :params (list "Bob")) ;; Soft-delete Dan (he's marked deleted but not gone) (delete contacts 4) (count-records contacts) ; → 4 (Dan is hidden) ;; Purge deleted records permanently (pack contacts) (count-records contacts) ; → 4
;; Extract records from a query and process them (def nyc-people (records (query contacts :where "city = ?" :params (list "New York")))) ;; Get names of NYC contacts (map (fn (r) (field-get r :name)) nyc-people) ; → ("Alice" "Carol") ;; Average age of all contacts (let ((all (records (query contacts))) (ages (map (fn (r) (field-get r :age)) all))) (/ (reduce + 0 ages) (length ages))) ; → 30.5
Let's build a personal expense tracker with category summaries.
(deftable expenses (description:string amount:number category:string date:string)) ;; Add some expenses (insert expenses {:description "Groceries" :amount 85.50 :category "food" :date "2026-02-01"}) (insert expenses {:description "Electric bill" :amount 120.00 :category "utilities" :date "2026-02-03"}) (insert expenses {:description "Coffee shop" :amount 12.75 :category "food" :date "2026-02-05"}) (insert expenses {:description "Bus pass" :amount 75.00 :category "transport" :date "2026-02-01"}) (insert expenses {:description "Restaurant" :amount 45.00 :category "food" :date "2026-02-10"}) (insert expenses {:description "Internet" :amount 65.00 :category "utilities" :date "2026-02-03"}) (insert expenses {:description "Books" :amount 32.99 :category "education" :date "2026-02-15"})
;; Total spending (let ((all (records (query expenses)))) (reduce + 0 (map (fn (r) (field-get r :amount)) all))) ; → 436.24 ;; Spending by category (defn category-total (cat) (let ((rows (records (query expenses :where "category = ?" :params (list cat))))) (reduce + 0 (map (fn (r) (field-get r :amount)) rows)))) (println (str "Food: $" (category-total "food"))) (println (str "Utilities: $" (category-total "utilities"))) (println (str "Transport: $" (category-total "transport"))) (println (str "Education: $" (category-total "education")))
;; Top 3 expenses by amount (query expenses :order "amount" :desc true :limit 3)
A classic mini-project that shows how to combine tables with helper functions.
(deftable todos (title:string priority:number done:bool)) ;; Helper: add a todo (defn add-todo (title priority) (insert todos {:title title :priority priority :done false}) (println (str "Added: " title))) ;; Helper: mark done (defn done! (id) (update todos id {:done true}) (println "Done!")) ;; Helper: show pending todos (defn pending () (query todos :where "done = ?" :params (list false) :order "priority")) ;; Helper: show completed todos (defn completed () (query todos :where "done = ?" :params (list true)))
;; Add tasks (lower priority number = more important) (add-todo "Write README" 1) (add-todo "Fix login bug" 1) (add-todo "Update tests" 2) (add-todo "Refactor CSS" 3) (add-todo "Deploy to staging" 2) ;; View pending tasks (pending) ;; Complete a task (done! 1) ; mark "Write README" as done ;; See what's left (pending) ;; See what's completed (completed)
.el file and load them with :load todo-helpers.el every time you open the REPL.
EELisp has two interactive view commands: browse shows a navigable table/grid, and edit opens a CRUD form. Let's build a recipe book using both.
Our recipe table uses several field types: string for short text, number for quantities, bool for flags, and memo for longer notes.
(deftable recipes
(name:string
cuisine:string
servings:number
vegetarian:bool
notes:memo))
(insert recipes {:name "Pasta Carbonara" :cuisine "Italian" :servings 4 :vegetarian false :notes "Use guanciale, not bacon."}) (insert recipes {:name "Vegetable Curry" :cuisine "Indian" :servings 6 :vegetarian true :notes "Toast spices first."}) (insert recipes {:name "Miso Soup" :cuisine "Japanese" :servings 2 :vegetarian true :notes "Don't boil the miso."})
The browse command opens a navigable table/grid showing all records at once:
;; Open the table view (browse recipes)
The table view shows all records in a grid. Click a row to select it, or use Prev/Next to navigate.
The edit command opens a dBASE-style form for full CRUD operations:
;; Open the CRUD form (edit recipes)
The form shows one record at a time. You can:
Both browse and edit support :where, :params, and :order:
;; Browse only vegetarian recipes (browse recipes :where "vegetarian = ?" :params (list true)) ;; Edit sorted by cuisine (edit recipes :order "cuisine") ;; Compare with text table output (query recipes :select (list "name" "cuisine" "servings"))
By default, data lives in memory and is lost when you close the app. To persist your recipes:
;; Create a database file in your folder :db new recipes ;; Now all deftable/insert/update calls are persisted ;; Check current database :db ;; Switch back to in-memory :db memory
recipes.lisp and load it with :load recipes.lisp. If your folder has an eelisp.db file, the REPL connects to it automatically when you open the folder.
The defform command creates standalone interactive forms with computed fields. Computed fields update automatically when you change input values — they are not saved to any database.
Let's build a calculator that computes compound interest from three inputs:
(defform compound-interest (principal:number rate:number years:number) :computed ((total-amount (* principal (pow (+ 1 (/ rate 100)) years))) (interest (- total-amount principal))))
This creates a form with three editable fields (principal, rate, years) and two computed fields (total-amount, interest). Try changing the values — computed fields update instantly.
The syntax is:
(defform form-name (field1:type field2:type ...) :computed ((computed1 expression1) (computed2 expression2)))
deftable: number, string, bool, date, memoTemperature converter:
(defform temperature (celsius:number) :computed ((fahrenheit (+ (* celsius 1.8) 32)) (kelvin (+ celsius 273.15))))
BMI calculator:
(defform bmi (weight-kg:number height-cm:number) :computed ((height-m (/ height-cm 100)) (bmi (/ weight-kg (* height-m height-m)))))
:load compound-calculator.lisp to see this in action.
EELisp includes a Lotus Agenda-inspired personal information manager built on top of the database layer. Think of it as a programmable to-do list meets knowledge organizer — you type free-form text, and structure emerges from categories.
Items are text-first. Just type what's on your mind:
;; Simple item (add-item "Finish quarterly report for Sarah") ;; With due date and priority (add-item "Call dentist to reschedule" :when "2026-03-01" :priority 2) ;; With a category and notes (add-item "Buy groceries" :category "personal" :notes "milk, eggs, bread, coffee") ;; High-priority work item (add-item "Deploy v2.1 to staging" :when "2026-02-26" :priority 1) ;; A meeting (add-item "Team standup" :when "2026-02-24" :priority 3)
The items command shows all items in a table view:
;; See all items (items)
Categories are hierarchical — use / separators. You can also define exclusive groups where an item can only belong to one child:
;; Work categories (defcategory work) (defcategory work/projects) (defcategory work/meetings) (defcategory work/admin) ;; Personal categories (defcategory personal) (defcategory personal/health) (defcategory personal/errands) ;; Exclusive group: item can only be high, medium, OR low (defcategory priority :exclusive true :children (high medium low)) ;; View the tree (categories)
Use assign to tag items. Exclusive categories automatically remove siblings:
;; Tag items with categories (assign 1 "work/projects") (assign 2 "personal/health") (assign 4 "work/projects") (assign 5 "work/meetings") ;; Set priorities (assign 1 "priority/high") (assign 4 "priority/high") (assign 2 "priority/medium") ;; Check an item (item-get 1)
;; Find all work items (items :category "work") ;; Search by text (items :search "quarterly") ;; Priority 1 items only (items :priority 1) ;; Items due before March (items :when-before "2026-03-01") ;; How many items do I have? (item-count)
;; Update properties (item-set 1 :priority 1 :when "2026-02-28") ;; Change priority — exclusive! Removes "high" automatically (assign 1 "priority/low") (item-get 1) ; now shows priority/low, not priority/high ;; Mark item as done (soft-deletes it) (item-done 3) ; "Buy groceries" — done! (item-count) ; → 4 (was 5) ;; Remove a category (unassign 2 "personal/health")
_items, _categories, _rules), created automatically. This means your agenda persists if you use a database file (:db myagenda.db). Categories are JSON arrays inside each item record, and properties (when, priority, etc.) are stored as a JSON object.
Define rules that automatically categorize items based on their content. This is what made Lotus Agenda revolutionary in 1988 — and now it's in EELisp.
;; Rule: items containing "URGENT" get high priority (defrule urgent-flag :when (str-contains text "URGENT") :assign "priority/high") ;; Rule: meetings and calls → work/meetings (defrule meeting-detect :when (or (str-contains text "meeting") (str-contains text "call with") (str-contains text "standup")) :assign "work/meetings") ;; Rule: shopping items → personal/errands (defrule errand-detect :when (or (str-contains text "buy ") (str-contains text "groceries")) :assign "personal/errands")
;; Apply rules to all items (returns count of changes) (apply-rules) ;; → 3 (items that matched rules) ;; Apply to a single item (apply-rules 2) ;; Check the results (item-get 2) ;; → now shows Categories: priority/high (if it contained "URGENT")
;; Turn on auto-apply — rules run on every add-item (auto-categorize true) ;; Now new items get categorized automatically! (add-item "URGENT: Deploy hotfix to production") ;; → automatically assigned priority/high (add-item "Buy milk and eggs this weekend") ;; → automatically assigned personal/errands (add-item "Standup with the team at 9am") ;; → automatically assigned work/meetings
;; Extract ISO dates from item text using :action (defrule date-extract :when (str-matches text "\\b(\\d{4}-\\d{2}-\\d{2})\\b") :action (item-set id :when (match 1))) ;; Items with dates get :when automatically (add-item "Deadline is 2026-04-01") (item-get 8) ;; → When: 2026-04-01
str-matches returns capture groups, and (match 1) extracts the first group.;; List all rules (rules) ;; → urgent-flag: (str-contains text "URGENT") ;; meeting-detect: (or (str-contains text "meeting") ...) ;; errand-detect: (or (str-contains text "buy ") ...) ;; date-extract: (str-matches text "\\b(\\d{4}-\\d{2}-\\d{2})\\b") ;; Delete a rule you no longer need (drop-rule "errand-detect") ;; Turn off auto-categorization (auto-categorize false)
_rules table. When evaluated, each item's fields (text, notes, id, categories, and all properties) are bound as symbols in the rule's evaluation environment. The :assign shorthand generates (assign id "category") actions, while :action lets you run any Lisp expression.
and/or/not, use has-category to check existing assignments, (get :property) for property tests, and str-matches for regex patterns. You can build arbitrarily complex categorization logic.
Views are saved queries that filter, sort, and group your items. Define a view once and show it anytime to see the latest matching items — like a live dashboard for your agenda.
Views use filter expressions — the same context as rules (text, categories, properties, has-category, overdue?):
;; A board of all work items, grouped by category (defview work-board :source items :filter (has-category "work") :group-by category :sort-by when) ;; High-priority items only (defview urgent :source items :filter (= priority "1") :sort-by when) ;; Items with no categories (uncategorized inbox) (defview inbox :source items :filter (= (length categories) 0)) ;; Overdue items (defview overdue :source items :filter (overdue?) :sort-by when)
Use show to display a view's results. Grouped views render as a tree with category headers:
;; Show the work board (show work-board)
;; Show urgent items (show urgent)
EELisp provides date helpers that are perfect for view filters:
;; Today's date as ISO string (today) ; → "2026-02-21" ;; Add days, weeks, or months (date-add (today) 7 :days) ; → "2026-02-28" (date-add (today) 1 :months) ; → "2026-03-21" ;; Difference in days (date-diff "2026-02-21" "2026-03-01") ; → 8
;; List all defined views (views) ;; → work-board: filter=(has-category "work") group-by=category sort-by=when ;; urgent: filter=(= priority "1") sort-by=when ;; inbox: filter=(= (length categories) 0) ;; overdue: filter=(overdue?) sort-by=when ;; Delete a view (drop-view "inbox")
_views table with their filter expressions serialized as Lisp. When you show a view, it fetches all items, evaluates the filter on each one (binding all item fields as symbols), sorts the matches, and optionally groups them. Items with missing properties are silently excluded — no errors.
overdue? helper in filters to find items past their :when date. Combine with and/or for complex views: :filter (and (has-category "work") (overdue?)) shows overdue work items.
Bridge your agenda with calendar dates. View what's scheduled for a specific day, browse a date range, and quickly add items for today — all from the REPL.
Use items-on to see everything scheduled for a specific date:
;; What's happening on Monday? (items-on "2026-02-24") ;; Use (today) for dynamic dates (items-on (today))
Use items-between to see items in a date range (inclusive on both ends):
;; What's this week? (items-between "2026-02-24" "2026-02-28") ;; Next 7 days using date helpers (items-between (today) (date-add (today) 7 :days)) ;; This whole month (items-between "2026-02-01" "2026-02-28")
add-item-today creates an item with :when automatically set to today's date:
;; Quick-add for today (add-item-today "Review pull requests" :priority 1) (add-item-today "Quick grocery run" :notes "milk, bread") (add-item-today "Team sync" :category "work/meetings")
Date helpers and calendar commands work great together with views:
;; Today's date as ISO string (today) ; → "2026-02-21" ;; Date arithmetic (date-add (today) 7 :days) ; → "2026-02-28" (date-add (today) 1 :months) ; → "2026-03-21" (date-diff "2026-02-21" "2026-03-01") ; → 8 (days) ;; Define a view for this week's items (defview this-week :source items :filter (and (>= when (today)) (<= when (date-add (today) 7 :days))) :sort-by when)
add-item-today is a shortcut for (add-item text :when (today)). It supports the same :priority, :category, and :notes options as add-item. Combine it with auto-categorization rules for zero-effort organization.
(items-on "date") — show items for a specific date(items-between "start" "end") — show items in a date range(add-item-today text ...) — add item with today's date(today) — current date as ISO string(date-add date n :days/:weeks/:months) — date arithmetic(date-diff date1 date2) — days between two datesThe add command uses regex-based natural language parsing to automatically extract dates, priorities, and people from your text. Combined with HTTP and JSON builtins, EELisp becomes a powerful automation tool.
Just type what's on your mind — add figures out the metadata:
;; Dates are extracted automatically (add "Meet Alice tomorrow for coffee") ;; → :when = tomorrow's date (add "Call Bob next Monday about the project") ;; → :when = next Monday's date, :who = Bob (add "Review docs end of month") ;; → :when = last day of current month (add "Deploy hotfix in 3 days") ;; → :when = 3 days from now
Express urgency with words or exclamation marks:
;; Word-based priority (add "URGENT fix server crash") ;; → :priority 1 ;; Exclamation marks: !!! = 1, !! = 2, ! = 3 (add "Review PR !!") ;; → :priority 2 (add "Update README low priority") ;; → :priority 4
People are detected from @mentions, prepositions, and action verbs:
;; @mentions (add "Review code for @carlos") ;; → :who = carlos ;; Verb + Name patterns (add "Email Sarah March 15 about renewal") ;; → :who = Sarah, :when = March 15 ;; Preposition + Name patterns (add "Meeting with John this weekend") ;; → :who = John, :when = Saturday
Use smart-parse to see what would be extracted without creating an item:
(smart-parse "Meet Alice tomorrow for coffee !!")
EELisp includes generic HTTP and JSON builtins for API integration:
;; Parse JSON to EELisp data (json-parse "{\"name\": \"Alice\", \"scores\": [95, 87]}") ;; → {:name "Alice" :scores (95 87)} ;; Convert EELisp to JSON (json-stringify {:name "Bob" :active true}) ;; → "{\"active\":true,\"name\":\"Bob\"}" ;; HTTP requests (http-get "https://api.example.com/data") ;; → {:status 200 :body "..."} (http-post "https://api.example.com/items" (json-stringify {:title "New item"}) :content-type "application/json") ;; → {:status 201 :body "..."}
2026-03-15), tomorrow/today/yesterday, next Monday, this weekend, in 3 days, in 2 weeks, end of week, end of month, March 15.
add works seamlessly with auto-categorization rules. After smart-parsing extracts metadata, rules still run — so (add "URGENT meeting with Bob tomorrow") can simultaneously set the date, priority, and trigger your "meeting-detect" rule to assign work/meetings.
Items that repeat on a schedule and reusable blueprints for common tasks.
Add :recur to any add-item call. Built-in patterns: :daily, :weekly, :monthly.
(add-item "Team standup" :when "2026-02-24" :recur :weekly :category "work") ;; Item #1: Team standup ;; When: 2026-02-24 ;; Recurrence: weekly ;; Categories: work
Use (every N :unit) for custom recurrence patterns:
(add-item "Quarterly review" :when "2026-04-01" :recur (every 3 :months)) (add-item "Biweekly report" :when "2026-02-28" :recur (every 2 :weeks))
When you mark a recurring item done, the system auto-creates the next occurrence:
(item-done 1) ;; Item #4: Team standup ;; When: 2026-03-03 ← advanced by 7 days ;; Recurrence: weekly ;; Categories: work
Templates are reusable blueprints for items you create frequently:
(deftemplate weekly-review :text "Weekly review: reflect on goals" :category "work/admin" :priority 2 :notes "1. What went well?\n2. What to improve?") ;; "Template 'weekly-review' defined"
Use from-template with optional overrides:
(from-template weekly-review :when "2026-03-07") ;; Item #5: Weekly review: reflect on goals ;; When: 2026-03-07 ;; Priority: 2 ;; Categories: work/admin ;; Override any field (from-template weekly-review :when "2026-03-14" :priority 1)
List and remove templates:
(templates) ;; weekly-review — "Weekly review: reflect on goals" [work/admin] !2 (drop-template weekly-review) ;; "Template 'weekly-review' removed"
:recur too — e.g. (deftemplate standup :text "Daily standup" :recur :daily). Every item created from it will automatically be recurring.Manage separate agendas for work, personal, and projects. Each agenda is an independent SQLite database with its own items, categories, rules, views, and templates.
Use open-agenda to create a new database file and switch to it:
;; Your default agenda is already active (add-item "Personal task" :category "personal") ;; Open a work agenda — creates the file if it doesn't exist (open-agenda "work.db") ;; "Opened agenda 'work' at /path/to/work.db" (add-item "Deploy to staging" :when "2026-03-01" :priority 1)
Use use-agenda to switch. Items are completely isolated between agendas:
;; Switch back to the default (use-agenda memory) (items) ;; → only "Personal task" ;; Switch to work (use-agenda work) (items) ;; → only "Deploy to staging"
;; See all open agendas (agendas) ;; Agendas: ;; memory — :memory: ;; work [active] — /path/to/work.db ;; Close an agenda you no longer need (close-agenda work)
Back up your agenda to JSON, or transfer items between agendas:
;; Export current agenda (export-agenda "memory" :format :json :path "backup.json") ;; "Exported agenda 'memory' to /path/to/backup.json" ;; Import into a different agenda (open-agenda "archive.db") (import-agenda "backup.json") ;; "Imported: 15 items, 4 categories, 3 rules, 2 views, 1 templates"
work.db becomes work, :memory: becomes memory. Use descriptive filenames for easy switching.EEditor's sidebar has three modes: Files, Calendar, and Agenda. The last two integrate directly with your EELisp agenda database, giving you a visual overview without writing queries.
Click the clipboard icon in the sidebar's segmented control to switch to Agenda mode. It automatically queries your database and organizes items into three sections:
:when date is before today (red header)Each item shows a priority dot (red / orange / yellow / gray), the item text, categories, and a formatted date. Press the refresh button to reload after adding items in the REPL.
The agenda sidebar doesn't duplicate database logic. Instead, the AgendaViewModel calls interpreter.eval() with the same EELisp builtins you'd use in the REPL:
;; What the Agenda sidebar executes internally: (items :when-before "2026-02-23") ;; → Overdue section (items-on "2026-02-23") ;; → Today section (items-between "2026-02-24" "2026-03-02") ;; → Upcoming section
The Calendar sidebar also integrates with your agenda. Each day cell shows dual dot indicators:
When you click a day, the detail panel shows both Files and Agenda sections, so you can see your writing activity alongside your scheduled tasks.
;; 1. Add some items with dates (add-item "Review PR" :when (today) :priority 1) (add-item "Team meeting" :when (date-add (today) 1 :days) :category "work/meetings") (add-item "Deploy v2" :when (date-add (today) 3 :days) :priority 2) (add-item "Overdue task" :when "2026-01-15") ;; 2. Switch to the Agenda sidebar tab ;; → You'll see: Overdue (1) | Today (1) | Next 7 Days (2) ;; 3. Switch to the Calendar sidebar tab ;; → Today has an orange dot; click it to see the item
Use the pipe macro to chain operations left-to-right instead of nesting:
;; Instead of this deeply nested expression: (str-join (map str-upper (filter (fn (s) (str-starts-with s "a")) (str-split "apple,avocado,banana,apricot" ","))) ", ") ;; Write this: (pipe "apple,avocado,banana,apricot" (str-split ",") (filter (fn (s) (str-starts-with s "a"))) (map str-upper) (str-join ", ")) ; → "APPLE, AVOCADO, APRICOT"
Write your functions in a file and load them into the REPL:
;; In the REPL, load a script file: :load ~/scripts/finance.el ;; Or right-click a .el file in the sidebar and ;; select "Run in REPL"
| Command | Description |
|---|---|
:help | Show all REPL commands |
:load <file> | Load and execute a script |
:db | Show current database path |
:db <path> | Switch to a database file |
:db new <name> | Create a new database file |
:db memory | Switch to in-memory database |
:env | List all defined symbols |
:clear | Clear the output |
:reset | Reset the interpreter |
| Pattern | Example |
|---|---|
| Transform a list | (map inc numbers) |
| Filter a list | (filter even? numbers) |
| Aggregate a list | (reduce + 0 numbers) |
| Query + process | (map (fn (r) (field-get r :name)) (records (query ...))) |
| Conditional | (if (> x 0) "positive" "non-positive") |
| Local scope | (let ((x 1) (y 2)) (+ x y)) |
| Loop | (loop ((i 0)) (= i 10) (recur (inc i))) |
| Math | (pow 2 10), (round 3.14159 2), (abs -5) |
| Table view | (browse tasks) |
| CRUD form | (edit tasks) |
| Calculator form | (defform name (fields) :computed ((name expr))) |
| Add agenda item | (add-item "text" :when "date" :priority n) |
| Browse items | (items :category "work" :search "text") |
| Define category | (defcategory work/projects) |
| Assign category | (assign 1 "work/projects") |
| Complete item | (item-done 1) |
| Define rule | (defrule name :when (str-contains text "URGENT") :assign "priority/high") |
| Auto-categorize | (auto-categorize true) |
| Apply rules | (apply-rules) |
| Regex rule | (defrule name :when (str-matches text "pattern") :action expr) |
| Define view | (defview name :source items :filter (has-category "work") :group-by category) |
| Show view | (show work-board) |
| Today's date | (today), (date-add (today) 7 :days) |
| Items for a date | (items-on "2026-02-24") |
| Items in range | (items-between (today) (date-add (today) 7 :days)) |
| Quick add today | (add-item-today "text" :priority 1) |
| Smart add (NLP) | (add "Meet Alice tomorrow !!") |
| Preview parsing | (smart-parse "Call Bob next Monday") |
| Parse JSON | (json-parse "{\"a\": 1}") |
| Stringify JSON | (json-stringify {:name "Alice"}) |
| HTTP GET | (http-get "https://api.example.com/data") |
| Recurring item | (add-item "Task" :when "2026-03-01" :recur :weekly) |
| Custom interval | (add-item "Review" :when "2026-04-01" :recur (every 3 :months)) |
| Define template | (deftemplate standup :text "Standup" :recur :daily) |
| Use template | (from-template standup :when "2026-03-01") |
| Open agenda | (open-agenda "work.db") |
| Switch agenda | (use-agenda work) |
| List agendas | (agendas) |
| Export agenda | (export-agenda "work" :format :json :path "backup.json") |
| Import agenda | (import-agenda "backup.json") |