🤖 This guide helps LLM agents add features to Cumulo applications based on calcium-workflow template.
📚 Related: Respo UI
What is Cumulo? Realtime fullstack framework using diff/patch for server-client sync.
Core Architecture (already in template):
- Server:
*reelatom →updater(pure fn) →twig-container→diff-twig→ patches - Client: WebSocket →
patch-twig→*storeatom → Respo render - Key libs:
recollect(diff/patch),cumulo-reel(time-travel),ws-edn(WebSocket)
Project structure (from template):
app.client # Client entry (already configured)
app.server # Server entry (already configured)
app.schema # ← ADD your data structures here
app.updater # ← ADD your business logic here
app.twig.* # ← ADD your view projections here
app.comp.* # ← ADD your UI components here
Example: Add a "tasks" feature where users can create/manage tasks.
cr query def app.schema/user
cr tree append-child app.schema/user -p "1" -e ":tasks (noted task ({}))"Add to app.schema namespace if needed:
def task
{}
:id nil
:title |
:done false
:created-at nilcr edit def app.updater.task/add-task -e \
'defn add-task (db title sid op-id op-time)
let
user-id $ get-in db ([] :sessions sid :user-id)
task $ {}
:id op-id
:title title
:done false
:created-at op-time
assoc-in db ([] :users user-id :tasks op-id) task'Key pattern: (db ...) → new-db (pure function, returns new state)
# Find insertion point
cr query def app.updater/updater
cr tree show app.updater/updater -p "2"
# Add new branch
cr tree insert-before app.updater/updater -p "2,8" -e \
'(:task/add title) (task/add-task db title sid op-id op-time)'cr query def app.twig.container/twig-container
# Then add/modify twig logicExample: Include tasks in user twig
defn twig-user (user)
-> user
dissoc :password
update :tasks $ fn (tasks)
; Only send recent 20 tasks
-> tasks vals
sort-by :created-at
reverse
take 20See Respo.md for UI development.
defn comp-task-list (tasks)
div ({} (:class-name css/task-list))
-> tasks vals
map $ fn (task)
[] (:id task)
comp-task-item taskTemplate:
defn your-updater (db data sid op-id op-time)
; 1. Extract user context
let
user-id $ get-in db ([] :sessions sid :user-id)
; 2. Validate if needed
if (valid? data)
; 3. Return new db (pure!)
assoc-in db ([] :users user-id :new-field) data
; 4. Or add error message
add-error-message db sid "|Validation failed"Constraints:
- ✅ Pure function (no I/O, no side effects)
- ✅ Always return
db - ✅ Use
get-in,assoc-in,update-infor nested updates - ❌ No
println,wss-send!, file operations inside updater
Template:
defn twig-your-feature (db session)
let
user-id $ :user-id session
if (some? user-id)
; Return only what this client should see
{}
:my-data $ get-in db ([] :users user-id :my-data)
:shared-data $ compute-shared-data db user-id
; Not logged in - return minimal data
{}Constraints:
- ✅ Filter by
session(never send all users' data) - ✅ Remove sensitive fields (
:password, etc.) - ✅ Consider
defn-memoizedfor expensive calculations - ❌ Don't return entire
db
Find location:
cr query def app.updater/updater
cr tree show app.updater/updater -p "2" # tag-match casesAdd case (use same structure as existing ones):
tag-match op
; Existing cases...
(:your-op/action arg1 arg2)
your-updater db arg1 arg2 sid op-id op-timecr tree append-child app.schema/user -p "1" -e ":new-field default-value"# 1. Find and read current code
cr query def app.updater.user/log-in
# 2. Locate exact position
cr tree show app.updater.user/log-in -p "2"
# 3. Replace specific node
cr tree replace app.updater.user/log-in -p "2,1,0" -e 'new-logic here'cr edit def app.twig.user/twig-user-profile -e \
'defn twig-user-profile (user)
{}
:id $ :id user
:name $ :name user
:stats $ compute-user-stats user'cr tree insert-after app.updater/updater -p "2,5" -e \
'(:new/operation data) (new-updater db data sid op-id op-time)'; ❌ BAD: Side effects
defn bad-updater (db sid)
println |Debug message ; Side effect!
wss-send! sid data ; Side effect!
check-write-file! ... ; I/O!
assoc-in db ...
; ✅ GOOD: Pure function
defn good-updater (db sid)
assoc-in db ([] :sessions sid :data) new-value
; ✅ Side effects in dispatch! (OK)
defn dispatch! (op sid)
println |Logging ; OK here
reset! *reel $ reel-reducer @*reel updater op sid op-id op-time; ❌ BAD: Leaks passwords
defn bad-twig (user)
user ; Contains :password!
; ✅ GOOD: Filter sensitive data
defn good-twig (user)
dissoc user :password
; ✅ BETTER: Explicit whitelist
defn safe-twig (user)
{}
:id $ :id user
:name $ :name user; ❌ BAD: Privacy violation
defn bad-twig-container (db session)
{}
:all-users (:users db) ; Everyone sees everyone!
; ✅ GOOD: Session-filtered
defn good-twig-container (db session)
if-let (user-id (:user-id session))
{}
:current-user $ twig-user $ get-in db ([] :users user-id)
:public-data $ get-public-data db
{}# View schema
cr query def app.schema/database
# Trace updater flow
cr query usages app.updater/updater
# Find where something is defined
cr query find your-function-name
# See all operations
cr query search app.updater/updater -p "tag-match" -l# Syntax check only (fast)
cr --check-only
# Run once and exit
cr -1
# Compile JS once
cr -1 js; In dispatch! and sync-clients!, you'll see:
; "Dispatch!" :user/log-in {...}
; "Changes for" "sid-123" : [[:assoc [:field] value]]ns app.twig.analytics $ :require
memof.alias :refer $ defn-memoized
; ✅ Memoize expensive calculations called in render loop
defn-memoized compute-user-stats (user-data timestamp)
; Heavy computation...
...Use when:
- Called in
twig-container(runs every 100ms) - Pure function with repeated inputs
- Expensive: sorting, filtering, aggregations
Don't use when:
- Function has side effects
- Arguments always change
; Always provide :key for list of objects
diff-twig old-tasks new-tasks $ {} (:key :id); Template already handles connect/disconnect
; You mainly add session-specific data:
defn add-user-data-to-session (db sid data op-id op-time)
assoc-in db ([] :sessions sid :user-data) dataExtend with custom logic:
defn user/log-in (db username password sid op-id op-time)
let
maybe-user $ find-user db username
if
and (some? maybe-user)
= (md5 password) (:password maybe-user)
; Success: Set user-id in session
assoc-in db ([] :sessions sid :user-id) (:id maybe-user)
; Failure: Add error message
add-error-message db sid "|Invalid credentials"; Client triggers route change
dispatch! $ :: :router/change ({} (:name :profile))
; Server stores in session (already in template)
; Twig returns page-specific data based on router# Development cycle
cr --check-only # Fast syntax check
cr -1 # Run once
cr js # Watch compile
mode=dev cr --entry server # Dev server
# Code exploration
cr query def app.updater/updater
cr query usages your-function
cr query find symbol-name
# Code modification
cr tree show path/to/func -p "2,1"
cr tree replace path/to/func -p "2,1,0" -e 'new-code'
cr edit def new/function -e 'defn ...'; Get user from session
let
user-id $ get-in db ([] :sessions sid :user-id)
user $ get-in db ([] :users user-id)
...
; Add nested field
assoc-in db ([] :users user-id :field) value
; Update nested field
update-in db ([] :users user-id :field) inc
; Check if logged in
if-let (user-id (get-in db ([] :sessions sid :user-id)))
; Logged in
...
; Not logged in
...
; Filter map
-> data
filter $ fn (entry)
condition? entry
; Transform list
-> items
map $ fn (item)
transform item
take 10defn add-error-message (db sid message)
let
msg-id $ generate-id!
assoc-in db ([] :sessions sid :messages msg-id)
{}
:id msg-id
:text message
:kind :error; Already in template's container component
comp-messages (:messages session)
{} $ :on-clear $ fn (msg-id)
dispatch! $ :: :session/remove-message msg-idcr --check-only # Syntax only (fast)
cr --check-only # Syntax only (fast)
cr -1 # Run full cycle once; ❌ WRONG
defn bad-updater (db sid)
println "|Logging in" ; NO!
wss-send! sid data ; NO!
assoc-in db ...
; ✅ CORRECT
defn good-updater (db sid)
assoc-in db ... ; Pure function
; ✅ Side effects belong in dispatch!
defn dispatch! (op sid)
println "|Debug" ; OK here
reset! *reel $ reel-reducer @*reel updater op sid op-id op-time; ❌ WRONG: Leaks password
defn bad-twig (user)
user
; ✅ CORRECT: Filter
defn good-twig (user)
dissoc user :password; ❌ WRONG: Everyone sees all users
defn bad-twig-container (db session)
{} $ :all-users (:users db)
; ✅ CORRECT: Filter by session
defn good-twig-container (db session)
if-let (user-id (:user-id session))
{} $ :user $ twig-user $ get-in db ([] :users user-id)
{}; ❌ WRONG
defn bad (db)
assoc! db :key val ; Mutates!
db
; ✅ CORRECT
defn good (db)
assoc db :key val ; Returns newWhen adding a feature:
- ✅ Schema - Define data structure in
app.schema - ✅ Updater - Pure function
(db ...) → new-dbinapp.updater.* - ✅ Wire - Add case to
app.updater/updatertag-match - ✅ Twig - Filter data by session in
app.twig.* - ✅ UI - Create Respo component in
app.comp.* - ✅ Test -
cr --check-onlybefore commit
Key Rules:
- Updaters must be pure (no I/O, no side effects)
- Twigs must filter by session (never leak private data)
- Always dissoc :password before sending to client
- Use
(:: :namespace/action args)for operations - Test with
cr --check-onlyfrequently
Template handles (don't need to modify often):
- WebSocket lifecycle (connect/disconnect)
- Render loop (100ms sync)
- Client patch application
- Basic session management
- Reel/time-travel setup
You mainly add:
- New schemas
- New updaters
- New twigs
- New UI components
Further Reading:
- Template source → https://github.com/Cumulo/calcium-workflow
Make sure read
cr docs agents --full
cr docs read respo.calcit --full