Obsidian Bases examples you can copy.
Bases turns the properties in your notes into tables and cards, with filters, formulas and totals, and no plugins. Here are ten recipes from our own vaults, each ready to paste, plus the mistakes that cost us time.
How to use these recipes
Bases is Obsidian’s built-in database view, a core plugin since version 1.9. It reads the properties at the top of your notes. Every recipe here was checked in Obsidian 1.14.
To try one, type ```base on its own line in any note, paste the recipe, and finish with ```. Or save it as a .base file in your vault: it’s plain text. Two recipes are just a formulas block; add those to a base like recipe 1.
1. Start here: one kind of note, minus templates
filters:
and:
- type == "task"
- '!file.inFolder("Templates")'
views:
- type: table
name: All tasks
order:
- file.name
- status
- dueFilter on a type property rather than a folder, so notes can live anywhere. The second line keeps your templates out of every view. Its single quotes matter: in YAML, a value starting with ! means something else.
2. Due today, and due this week
filters:
and:
- type == "task"
- '!file.inFolder("Templates")'
views:
- type: table
name: Today
filters:
and:
- status != "Done"
- if(due, date(due) <= today(), false)
order:
- file.name
- due
sort:
- property: due
direction: ASC
- type: table
name: This week
filters:
and:
- status != "Done"
- if(due, (date(due) - today()).days <= 7, false)
order:
- file.name
- status
- due
sort:
- property: due
direction: ASCWrapping the date test in if(due, …, false) skips tasks with no due date. “Today” also catches anything overdue, which is usually what you want first thing.
3. Days since something happened
formulas:
days_quiet: if(last_contact, (today() - date(last_contact)).days.round(0), "")Subtracting two dates gives a length of time, and .days turns it into a number of days. Always round it: across a daylight-saving change the gap isn’t a whole number of days.
The same test works as a view filter: if(last_contact, (today() - date(last_contact)).days > 30, true) lists everyone you haven’t spoken to in a month, including anyone with no date at all.
4. A countdown in words
formulas:
follow_up: if(next_date, if(date(next_date) <= today(), "⏰ Due", "in " + (date(next_date) - today()).days.round(0).toString() + " days"), "")Shows “⏰ Due” or “in 3 days”. .toString() turns a number into text and + joins text. Nest more if()s for more cases.
5. Late invoices, with a total
filters:
and:
- type == "invoice"
- '!file.inFolder("Templates")'
formulas:
days_late: if(status == "Sent", if(due, if(date(due) < today(), (today() - date(due)).days.round(0), ""), ""), "")
views:
- type: table
name: Late
filters:
and:
- status == "Sent"
- if(due, date(due) < today(), false)
order:
- file.name
- client
- amount
- due
- formula.days_late
summaries:
amount: Sum
- type: table
name: Paid this year
filters:
and:
- status == "Paid"
- if(paid_on, date(paid_on).year == today().year, false)
order:
- file.name
- client
- amount
- paid_on
summaries:
amount: Sumsummaries adds a row under the table, and Sum totals the column. The second view uses .year to keep only this year’s payments.
6. Weighted pipeline value
filters:
and:
- type == "deal"
- '!file.inFolder("Templates")'
formulas:
weighted: if(value, value * if(probability, probability, 0) / 100, 0)
views:
- type: table
name: Open deals
filters:
and:
- stage != "Won"
- stage != "Lost"
order:
- file.name
- value
- probability
- formula.weighted
summaries:
value: Sum
formula.weighted: SumA $10,000 deal at 30% counts as $3,000. The inner if() treats a missing probability as zero, so one blank field doesn’t spoil the total.
7. Groups in your order, not alphabetical
filters:
and:
- type == "deal"
- '!file.inFolder("Templates")'
formulas:
stage_step: if(stage == "Lead", "1 Lead", if(stage == "Proposal sent", "2 Proposal sent", if(stage == "Negotiating", "3 Negotiating", if(stage == "Won", "4 Won", "0 No stage"))))
views:
- type: cards
name: Board
filters:
and:
- stage != "Lost"
groupBy:
property: formula.stage_step
direction: ASC
order:
- file.name
- valueGroups sort by name, so “Negotiating” would land before “Proposal sent”. A formula that puts a number in front keeps them in the order work actually flows.
8. Renewals with a traffic light
filters:
and:
- type == "renewal"
- '!file.inFolder("Templates")'
formulas:
days_left: if(renews_on, (date(renews_on) - today()).days.round(0), "")
alert: if(!renews_on, "", if(date(renews_on) < today(), "🔴 Lapsed", if((date(renews_on) - today()).days.round(0) <= 30, "🟡 Soon", "🟢 Later")))
views:
- type: table
name: Next 60 days
filters:
and:
- if(renews_on, (date(renews_on) - today()).days <= 60, false)
order:
- file.name
- renews_on
- formula.days_left
- auto_renew
- cost
- formula.alert
sort:
- property: renews_on
direction: ASC
summaries:
cost: Sum!renews_on means “has no date”. The view shows the next 60 days, including anything already lapsed, and totals what it will cost.
9. Flag what’s missing
filters:
and:
- type == "device"
- '!file.inFolder("Templates")'
formulas:
warranty: if(!warranty_until, "", if(date(warranty_until) < today(), "Expired", if((date(warranty_until) - today()).days.round(0) <= 60, "Ends soon", "OK")))
problems: if(!encrypted, "Not encrypted. ", "") + if(!backup, "No backup. ", "") + if(!auto_updates, "Updates off.", "")
properties:
file.name:
displayName: Device
note.assigned_to:
displayName: Assigned to
formula.problems:
displayName: Needs
views:
- type: table
name: Needs attention
filters:
and:
- status == "In use"
- '!encrypted || !backup || !auto_updates'
order:
- file.name
- assigned_to
- formula.problems
- type: table
name: All devices
filters:
and:
- status != "Retired"
groupBy:
property: kind
direction: ASC
order:
- file.name
- assigned_to
- status
- formula.warrantyFor true/false (checkbox) properties, !encrypted means “not ticked” and || means “or”. The problems formula joins short notes with +, so each row reads like a to-do. Quote any filter that starts with !.
10. One base, many pages
filters:
and:
- type == "invoice"
- '!file.inFolder("Templates")'
views:
- type: table
name: This client
filters:
and:
- file.hasLink(this.file)
order:
- file.name
- amount
- due
- statusEmbed it in a client note with ![[Invoices.base#This client]]. Inside an embed, this.file is the note doing the embedding, so each client page lists only its own invoices (any invoice whose properties link to that client). Put the embed in your template once and every new page gets it.
Gotchas and a quick reference
- Renaming columns: under
properties, usenote.for your fields,formula.for formulas andfile.for file details. - Dates:
today(),date(x)to read a property as a date,.daysfor a difference in days,.yearfor the year. - Logic:
if(test, yes, no),&&for and,||for or,!for not. - Files:
file.name,file.inFolder("…"),file.hasLink(this.file). - Empty values: guard with
if(field, …, fallback)before doing date maths. - Embeds:
![[File.base#View name]]shows one view; the name must match exactly.
Keep going.
How to Build a Simple CRM in Obsidian (No Plugins)
Track clients, follow-ups and invoices with one note per client and a single Base. Copy-paste code, no plugins.
Read the guide Small office ITSmall Office IT Documentation in Obsidian: What to Record
What a small office should write down about its IT, what never to store, and a simple setup in Obsidian.
Read the guide Building with AIA Spec Template for AI Coding Tools (Claude Code, Cursor)
A one-page spec template for Claude Code, Cursor and other AI coding tools, a plan-first prompt, and where Obsidian fits.
Read the guide