EELisp Tutorial

Learn EELisp by building real projects. From your first expression to a compound interest calculator, expense tracker, and Lotus Agenda-inspired personal organizer.

Contents

  1. Your First Expression Basics
  2. Variables & Functions Basics
  3. Lists & Higher-Order Functions Basics
  4. Dictionaries Basics
  5. Compound Interest Calculator Project
  6. Contact Manager with Tables Project
  7. Expense Tracker Project
  8. Building a Todo List Project
  9. Recipe Book with Browse & Edit Project
  10. Calculator Forms with Computed Fields Project
  11. Personal Agenda: Items & Categories Project
  12. Auto-Categorization Rules Project
  13. Views — Dynamic Perspectives Project
  14. Calendar Integration Project
  15. Smart Input & HTTP Project
  16. Recurring Items & Templates Project
  17. Multiple Agendas Project
  18. Agenda Sidebar & Calendar Integration Project
  19. Tips & Patterns Reference

1. Your First Expression

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
Tip: You don't need spaces between operators and numbers. (+1 2) works the same as (+ 1 2).

2. Variables & Functions

Defining variables

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."

Defining functions

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

Anonymous functions

Use fn for one-off functions without a name:

(map (fn (x) (* x x)) '(1 2 3 4 5))
; → (1 4 9 16 25)

3. Lists & Higher-Order Functions

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)

Map, Filter, Reduce

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)

4. Dictionaries

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"}

5. Project: Compound Interest Calculator

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.

Define the core function

;; 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!)

Add a yearly breakdown

;; 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)
> (yearly-breakdown 10000 7 12 5) Investment: $10000 at 7% Compounding: 12x/year for 5 years --- Year 1: $10723 (+723 interest) Year 2: $11497 (+1497 interest) Year 3: $12327 (+2327 interest) Year 4: $13217 (+3217 interest) Year 5: $14171 (+4171 interest)

Compare different rates

;; 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)
> (compare-rates 10000 20 3 5 7 10) Comparing $10000 over 20 years: 3% → $18167 (+8167) 5% → $27126 (+17126) 7% → $40387 (+30387) 10% → $72890 (+62890)
Lesson learned: Functions in EELisp can take rest parameters (. rates) to accept a variable number of arguments. This is perfect for comparing multiple values.

6. Project: Contact Manager with Tables

EELisp has a built-in SQLite database. Let's use it to manage contacts — just like a mini dBASE.

Create the table

;; 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 records

;; 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})

Query your data

;; 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 and delete

;; 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

Work with query results in code

;; 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

7. Project: Expense Tracker

Let's build a personal expense tracker with category summaries.

Set up the table

(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"})

Query and analyze

;; 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")))
> Food: $143.25 Utilities: $185 Transport: $75 Education: $32.99

Find the biggest expenses

;; Top 3 expenses by amount
(query expenses :order "amount" :desc true :limit 3)

8. Project: Building a Todo List

A classic mini-project that shows how to combine tables with helper functions.

Create the table and helpers

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

Use it

;; 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)
Tip: You can save these function definitions in a .el file and load them with :load todo-helpers.el every time you open the REPL.

9. Project: Recipe Book with Browse & Edit

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.

Create the table

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

Add some recipes

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

Browse your recipes (table view)

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.

Edit with a CRUD form

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:

Filtering and sorting

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"))

Persistent storage

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
Tip: Save this as 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.

10. Project: Calculator Forms with Computed Fields

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.

Compound interest calculator

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.

How defform works

The syntax is:

(defform form-name
  (field1:type field2:type ...)
  :computed
  ((computed1 expression1)
   (computed2 expression2)))

More examples

Temperature 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)))))
Tip: Load the example file with :load compound-calculator.lisp to see this in action.

11. Project: Personal Agenda — Items & Categories

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.

Add your first items

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)
> (add-item "Finish quarterly report for Sarah") Item #1: Finish quarterly report for Sarah > (add-item "Call dentist to reschedule" :when "2026-03-01" :priority 2) Item #2: Call dentist to reschedule When: 2026-03-01 Priority: 2

Browse your items

The items command shows all items in a table view:

;; See all items
(items)
> (items) id | text | when | priority | categories ---+-----------------------------------+------------+----------+----------- 1 | Finish quarterly report for Sarah | | | 2 | Call dentist to reschedule | 2026-03-01 | 2 | 3 | Buy groceries | | | personal 4 | Deploy v2.1 to staging | 2026-02-26 | 1 | 5 | Team standup | 2026-02-24 | 3 | 5 record(s)

Define categories

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)
> (categories) personal personal/errands personal/health priority [exclusive] priority/high priority/low priority/medium work work/admin work/meetings work/projects

Assign categories to items

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)
> (item-get 1) Item #1: Finish quarterly report for Sarah Categories: priority/high, work/projects

Filter and search

;; 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 and complete items

;; 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")
How it works: Items and categories are stored in SQLite tables (_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.
Tip: Combine agenda items with the database layer. You could write a helper that queries your items and generates a Markdown daily summary file.

12. Auto-Categorization Rules — The Lotus Agenda Magic

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.

Define text-matching rules

;; 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")
Rules defined: urgent-flag, meeting-detect, errand-detect

Apply rules to existing items

;; 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")

Enable auto-categorization

;; 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

Use regex for date extraction

;; 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.

Manage your rules

;; 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)
How it works: Rule conditions are stored as serialized Lisp expressions in the _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.
Tip: Rules are full Lisp expressions — combine 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.

13. Views — Dynamic Perspectives

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.

Define your first views

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)

Show a view

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 work-board) ▸ work/meetings (2) Team standup Call with client ▸ work/projects (3) Deploy v2.1 to staging Finish quarterly report Code review PR #42
;; Show urgent items
(show urgent)
> (show urgent) id | text | when | priority ---+-----------------------------------+------------+--------- 4 | Deploy v2.1 to staging | 2026-02-26 | 1 6 | URGENT: Deploy hotfix | | 1 2 record(s)

Use date helpers for time-based views

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

Manage your views

;; 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")
How it works: Views are stored in the _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.
Tip: Use the 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.

14. Calendar Integration

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.

View items by date

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))
> (items-on "2026-02-24") id | text | when | priority | categories ---+-----------------------------------+------------+----------+----------- 5 | Team standup | 2026-02-24 | 3 | work/meetings 1 record(s)

Browse a date range

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")
> (items-between "2026-02-24" "2026-02-28") id | text | when | priority | categories ---+-----------------------------------+------------+----------+----------- 5 | Team standup | 2026-02-24 | 3 | work/meetings 4 | Deploy v2.1 to staging | 2026-02-26 | 1 | work/projects 1 | Finish quarterly report | 2026-02-28 | | work/projects 3 record(s)

Quick-add items for today

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")
> (add-item-today "Review pull requests" :priority 1) Item #9: Review pull requests When: 2026-02-21 Priority: 1

Combine with date helpers and views

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)
Tip: 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.
Calendar commands reference:

15. Smart Input & HTTP

The 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.

Natural language item creation

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
> (add "Meet Alice tomorrow for coffee") Item #1: Meet Alice for coffee When: 2026-02-23 Who: Alice

Priority detection

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 and @mentions

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

Preview with smart-parse

Use smart-parse to see what would be extracted without creating an item:

(smart-parse "Meet Alice tomorrow for coffee !!")
> (smart-parse "Meet Alice tomorrow for coffee !!") {:text "Meet Alice for coffee" :when "2026-02-23" :priority 2 :who ("Alice")}

HTTP and JSON builtins

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 "..."}
Recognized date patterns: ISO dates (2026-03-15), tomorrow/today/yesterday, next Monday, this weekend, in 3 days, in 2 weeks, end of week, end of month, March 15.
Tip: 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.

16. Recurring Items & Templates

Items that repeat on a schedule and reusable blueprints for common tasks.

Step 1 — Create a recurring item

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

Step 2 — Custom intervals

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

Step 3 — Complete a recurring item

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
Tip: The new item preserves all properties — text, categories, priority, recurrence pattern, and notes. Only the date advances.

Step 4 — Define a template

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"

Step 5 — Create items from templates

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)

Step 6 — Manage templates

List and remove templates:

(templates)
;; weekly-review — "Weekly review: reflect on goals" [work/admin] !2

(drop-template weekly-review)
;; "Template 'weekly-review' removed"
Tip: Templates can include :recur too — e.g. (deftemplate standup :text "Daily standup" :recur :daily). Every item created from it will automatically be recurring.

17. Multiple Agendas

Manage separate agendas for work, personal, and projects. Each agenda is an independent SQLite database with its own items, categories, rules, views, and templates.

Step 1: Open a second agenda

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)

Step 2: Switch between agendas

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"

Step 3: List and manage agendas

;; See all open agendas
(agendas)
;; Agendas:
;;   memory — :memory:
;;   work [active] — /path/to/work.db

;; Close an agenda you no longer need
(close-agenda work)

Step 4: Export and import

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"
Tip: Agenda names are derived from filenames — work.db becomes work, :memory: becomes memory. Use descriptive filenames for easy switching.

18. Agenda Sidebar & Calendar Integration

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.

The Agenda Sidebar

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:

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.

How it works behind the scenes

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

Calendar + Agenda

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.

Try it yourself

;; 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
Tip: The GUI sidebar and the REPL share the same interpreter and database. Any item you add in the REPL appears in the sidebar after a refresh, and vice versa. This is the power of the EELisp-GUI integration — one data engine, multiple views.

19. Tips & Patterns

Pipe for readability

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"

Save and load scripts

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"

REPL commands cheat sheet

CommandDescription
:helpShow all REPL commands
:load <file>Load and execute a script
:dbShow current database path
:db <path>Switch to a database file
:db new <name>Create a new database file
:db memorySwitch to in-memory database
:envList all defined symbols
:clearClear the output
:resetReset the interpreter

Common patterns

PatternExample
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")
What's next? Try combining tables and functions to build your own tools: a reading log, a recipe book, a workout tracker, or a personal CRM. The REPL is your playground.