Obsidian workflow for car shopping (or other complex, comparative tasks)

Intro

Hello! I really like this workflow that I have developed for used car shopping. It’s such a shitty process that I wanted to share this with others in hopes that it helps. I can imagine it being adapted to say apartment hunting. Anything where there’s a lot of information to sort through and the process can be thorny. This is a big post because I have included preview images, code blocks and clipper templates to make this as easy as possible for other people to learn, utilize and personalize this workflow.

One thing that I want to note is that I am not a coder. I’m sure there’s more optimal ways that some of this could be set up. But I’m working with what I have and what I know. With all that said, let’s dive in!

Process

1. Find what I want and clip the page

First, I go to Carfax and set up my query. I open lots of tabs and do an initial filtering. For anything that is acceptable (not just ideal), I grab the car’s information using my web clipper template, which sends it into my vault. On that note, I make, well, a note of anything in particular that sticks out about it or specific questions that I should bring up to the seller/dealer. If some of the info wasn’t available, I comment out those lines in my Datacore query so that it doesn’t return a failure.

Note that the page is named for the VIN number of the car so as to utilize a unique identifier. Because that’s not practical to read, I also put the car name/make/model/trim in the “title” property and in the aliases. This also allows me to grab it for my various tables, including the one in the clipper (note) itself.

Also note that I have a Meta-Bind math function that compares the list price to Carfax’s “expected value”. This can be pretty off base compared to some other sources (see the next step) but it’s useful information that can easily be grabbed and seen at a glance.

Clipper Template

{
  "schemaVersion": "0.1.0",
  "name": "Carfax car grab",
  "behavior": "create",
  "noteContentFormat": "\n```datacorejsx\n// A list of columns to show in the table.\nconst COLUMNS = [\n\t{\n\t    id: 'Name',\n\t    value: row =>{\n\t\t    return dc.coerce.link(`[[${row.$name}|${row.value('title')}]]`)\n\t    }\n    },\n    { id: 'Rating', value: page => page.value('rating') },\n\t{\n\t\tid: \"Cover\",\n\t\tvalue: row => {\n\t\t  /* User Config */\n\t\t  const field = \"cover\"\n\t\t  const width = 100\n\t\t  const alt = \"\"\n\t\t  \n\t\t  /* Logic */\n\t\t  const link = row.value(field)\n\t\t  const image = String(link)\n\t\t  const metadata = `|${alt}|${width}`\n\t\t\t\n\t\t  if (image.startsWith('http')) {\n\t\t\treturn `![${metadata}](${image})`\n\t\t  } else {\n\t\t\treturn `![[${link.path}${metadata}]]`\n\t\t  }\n\t\t}\n    },\n\t{\n\t\tid: \"Risk\",\n\t\tvalue: row => {\n\t\t  /* User Config */\n\t\t  const field = \"riskImage\"\n\t\t  const width = 100\n\t\t  const alt = \"\"\n\t\t  \n\t\t  /* Logic */\n\t\t  const link = row.value(field)\n\t\t  const image = String(link)\n\t\t  const metadata = `|${alt}|${width}`\n\t\t\t\n\t\t  if (image.startsWith('http')) {\n\t\t\treturn `![${metadata}](${image})`\n\t\t  } else {\n\t\t\treturn `![[${link.path}${metadata}]]`\n\t\t  }\n\t\t}\n    },\n\t{\n\t\tid: \"Cost\",\n\t\tvalue: row => {\n\t\t  /* User Config */\n\t\t  const field = \"costImage\"\n\t\t  const width = 100\n\t\t  const alt = \"\"\n\t\t  \n\t\t  /* Logic */\n\t\t  const link = row.value(field)\n\t\t  const image = String(link)\n\t\t  const metadata = `|${alt}|${width}`\n\t\t\t\n\t\t  if (image.startsWith('http')) {\n\t\t\treturn `![${metadata}](${image})`\n\t\t  } else {\n\t\t\treturn `![[${link.path}${metadata}]]`\n\t\t  }\n\t\t}\n    },\n];\n\nreturn function View() {\n    // Selecting `#game` pages, for example.\n    const pages = dc.useQuery('@page and $link = [[{{selector:div.vehicle-module__details|replace:\"VIN: \":\"\"|replace:\"/ · Stock #\\: .+/g\":\"\"}}]]');\n\n    // Uses the built in 'vanilla table' component for showing objects in a table!\n    return <dc.Table columns={COLUMNS} rows={pages} />;\n}\n```\n\n## Damage\n{{selector:details.accordion_expander:nth-child(1) > summary:nth-child(1) > p:nth-child(2)}}\n## Use History\n{{selector:details.accordion_expander:nth-child(2) > div:nth-child(2) > div:nth-child(1)}}\n## Owner History\n{{selector:details.accordion_expander:nth-child(3) > div:nth-child(2) > div:nth-child(1)}}\n## Service History\n{{selector:details.accordion_expander:nth-child(4) > div:nth-child(2) > div:nth-child(1)}}\n\n## Bits & Bobs\n#car/2025\n\n```meta-bind\nVIEW[{listing-price} - {fair-market-price}][math:difference]\n```\n\n[Carfax Report]({{selector:a.button:nth-child(5)?href}})\n\n[{{selector:a.button:nth-child(2)}}]({{selector:a.button:nth-child(2)?href}})",
  "properties": [
    {
      "name": "rating",
      "value": "{{selector:.overall-rating-value}}",
      "type": "number"
    },
    {
      "name": "source",
      "value": "{{url}}",
      "type": "text"
    },
    {
      "name": "fair-market-price",
      "value": "{{selector:.carfax-value__value-section > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(2) > span:nth-child(1)|replace:\\\"$\\\":\\\"\\\"|replace:\\\" CARFAX Value\\\":\\\"\\\"|replace:\\\",\\\":\\\"\\\"}}",
      "type": "number"
    },
    {
      "name": "listing-price",
      "value": "{{selector:.disclaimer-vehicle-price|replace:\\\"$\\\":\\\"\\\"|replace:\\\",\\\":\\\"\\\"}}",
      "type": "number"
    },
    {
      "name": "difference",
      "value": "",
      "type": "number"
    },
    {
      "name": "cover",
      "value": "{{selector:img.fetchpriority?src}}",
      "type": "text"
    },
    {
      "name": "VIN",
      "value": "{{selector:div.vehicle-module__details|replace:\\\"VIN: \\\":\\\"\\\"|replace:\\\"/ · Stock #\\: .+/g\\\":\\\"\\\"}}",
      "type": "text"
    },
    {
      "name": "dealer",
      "value": "[[{{selector:.dealer-name}}]]",
      "type": "text"
    },
    {
      "name": "miles",
      "value": "{{selector:.reliability-info__top__car-info__mileage|replace:\\\",\\\":\\\"\\\"|replace:\\\" miles\\\":\\\"\\\"}}",
      "type": "number"
    },
    {
      "name": "location",
      "value": "{{selector:span#dealer-info__address-text}}",
      "type": "text"
    },
    {
      "name": "costOfRepair",
      "value": "{{selector:.reliability-info__bottom__cost__subtext|replace:\\\"$\\\":\\\"\\\"|replace:\\\" avg. cost per year\\\":\\\"\\\"}}",
      "type": "text"
    },
    {
      "name": "costImage",
      "value": "{{selector:.reliability-info__bottom__cost > img:nth-child(1)?src}}",
      "type": "text"
    },
    {
      "name": "riskImage",
      "value": "{{selector:.reliability-info__bottom__risk > img:nth-child(1)?src}}",
      "type": "text"
    },
    {
      "name": "year",
      "value": "{{selector:#vdp-ymm-ratings > h2:nth-child(1)|replace\\\"/[^\\d{4}]\\\":\\\"\\\"}}",
      "type": "number"
    },
    {
      "name": "available",
      "value": "true",
      "type": "checkbox"
    },
    {
      "name": "title",
      "value": "{{selector:#_automation_test_VDP_h1|replace:\\\"Used \\\":\\\"\\\"|safe_title}}",
      "type": "text"
    },
    {
      "name": "aliases",
      "value": "{{selector:#_automation_test_VDP_h1|replace:\\\"Used \\\":\\\"\\\"|safe_title}}",
      "type": "multitext"
    }
  ],
  "triggers": [
    "www.carfax.com/vehicle/"
  ],
  "noteNameFormat": "{{selector:div.vehicle-module__details|replace:\"VIN: \":\"\"|replace:\"/ · Stock #\\: .+/g\":\"\"|safe_name}}",
  "path": "Atlas/2025 Car Search"
}

2. Optional: Check Consumer Reports

I also have a clipper template that I use to grab pertinent information from Consumer Reports (which I can access for free through my local library. Yay!) This gets saved into the vault too, but I’m still working on integrating the querying more closely with the actual cars.
These are named for the year/make/model, which makes it queryable based on the “title” property of the car note.

Clipper Template

{
  "schemaVersion": "0.1.0",
  "name": "Carfax car grab",
  "behavior": "create",
  "noteContentFormat": "\n```datacorejsx\n// A list of columns to show in the table.\nconst COLUMNS = [\n\t{\n\t    id: 'Name',\n\t    value: row =>{\n\t\t    return dc.coerce.link(`[[${row.$name}|${row.value('title')}]]`)\n\t    }\n    },\n    { id: 'Rating', value: page => page.value('rating') },\n\t{\n\t\tid: \"Cover\",\n\t\tvalue: row => {\n\t\t  /* User Config */\n\t\t  const field = \"cover\"\n\t\t  const width = 100\n\t\t  const alt = \"\"\n\t\t  \n\t\t  /* Logic */\n\t\t  const link = row.value(field)\n\t\t  const image = String(link)\n\t\t  const metadata = `|${alt}|${width}`\n\t\t\t\n\t\t  if (image.startsWith('http')) {\n\t\t\treturn `![${metadata}](${image})`\n\t\t  } else {\n\t\t\treturn `![[${link.path}${metadata}]]`\n\t\t  }\n\t\t}\n    },\n\t{\n\t\tid: \"Risk\",\n\t\tvalue: row => {\n\t\t  /* User Config */\n\t\t  const field = \"riskImage\"\n\t\t  const width = 100\n\t\t  const alt = \"\"\n\t\t  \n\t\t  /* Logic */\n\t\t  const link = row.value(field)\n\t\t  const image = String(link)\n\t\t  const metadata = `|${alt}|${width}`\n\t\t\t\n\t\t  if (image.startsWith('http')) {\n\t\t\treturn `![${metadata}](${image})`\n\t\t  } else {\n\t\t\treturn `![[${link.path}${metadata}]]`\n\t\t  }\n\t\t}\n    },\n\t{\n\t\tid: \"Cost\",\n\t\tvalue: row => {\n\t\t  /* User Config */\n\t\t  const field = \"costImage\"\n\t\t  const width = 100\n\t\t  const alt = \"\"\n\t\t  \n\t\t  /* Logic */\n\t\t  const link = row.value(field)\n\t\t  const image = String(link)\n\t\t  const metadata = `|${alt}|${width}`\n\t\t\t\n\t\t  if (image.startsWith('http')) {\n\t\t\treturn `![${metadata}](${image})`\n\t\t  } else {\n\t\t\treturn `![[${link.path}${metadata}]]`\n\t\t  }\n\t\t}\n    },\n];\n\nreturn function View() {\n    // Selecting `#game` pages, for example.\n    const pages = dc.useQuery('@page and $link = [[{{selector:div.vehicle-module__details|replace:\"VIN: \":\"\"|replace:\"/ · Stock #\\: .+/g\":\"\"}}]]');\n\n    // Uses the built in 'vanilla table' component for showing objects in a table!\n    return <dc.Table columns={COLUMNS} rows={pages} />;\n}\n```\n\n## Damage\n{{selector:details.accordion_expander:nth-child(1) > summary:nth-child(1) > p:nth-child(2)}}\n## Use History\n{{selector:details.accordion_expander:nth-child(2) > div:nth-child(2) > div:nth-child(1)}}\n## Owner History\n{{selector:details.accordion_expander:nth-child(3) > div:nth-child(2) > div:nth-child(1)}}\n## Service History\n{{selector:details.accordion_expander:nth-child(4) > div:nth-child(2) > div:nth-child(1)}}\n\n## Bits & Bobs\n#car/2025\n\n```meta-bind\nVIEW[{listing-price} - {fair-market-price}][math:difference]\n```\n\n[Carfax Report]({{selector:a.button:nth-child(5)?href}})\n\n[{{selector:a.button:nth-child(2)}}]({{selector:a.button:nth-child(2)?href}})",
  "properties": [
    {
      "name": "rating",
      "value": "{{selector:.overall-rating-value}}",
      "type": "number"
    },
    {
      "name": "source",
      "value": "{{url}}",
      "type": "text"
    },
    {
      "name": "fair-market-price",
      "value": "{{selector:.carfax-value__value-section > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(2) > span:nth-child(1)|replace:\\\"$\\\":\\\"\\\"|replace:\\\" CARFAX Value\\\":\\\"\\\"|replace:\\\",\\\":\\\"\\\"}}",
      "type": "number"
    },
    {
      "name": "listing-price",
      "value": "{{selector:.disclaimer-vehicle-price|replace:\\\"$\\\":\\\"\\\"|replace:\\\",\\\":\\\"\\\"}}",
      "type": "number"
    },
    {
      "name": "difference",
      "value": "",
      "type": "number"
    },
    {
      "name": "cover",
      "value": "{{selector:img.fetchpriority?src}}",
      "type": "text"
    },
    {
      "name": "VIN",
      "value": "{{selector:div.vehicle-module__details|replace:\\\"VIN: \\\":\\\"\\\"|replace:\\\"/ · Stock #\\: .+/g\\\":\\\"\\\"}}",
      "type": "text"
    },
    {
      "name": "dealer",
      "value": "[[{{selector:.dealer-name}}]]",
      "type": "text"
    },
    {
      "name": "miles",
      "value": "{{selector:.reliability-info__top__car-info__mileage|replace:\\\",\\\":\\\"\\\"|replace:\\\" miles\\\":\\\"\\\"}}",
      "type": "number"
    },
    {
      "name": "location",
      "value": "{{selector:span#dealer-info__address-text}}",
      "type": "text"
    },
    {
      "name": "costOfRepair",
      "value": "{{selector:.reliability-info__bottom__cost__subtext|replace:\\\"$\\\":\\\"\\\"|replace:\\\" avg. cost per year\\\":\\\"\\\"}}",
      "type": "text"
    },
    {
      "name": "costImage",
      "value": "{{selector:.reliability-info__bottom__cost > img:nth-child(1)?src}}",
      "type": "text"
    },
    {
      "name": "riskImage",
      "value": "{{selector:.reliability-info__bottom__risk > img:nth-child(1)?src}}",
      "type": "text"
    },
    {
      "name": "year",
      "value": "{{selector:#vdp-ymm-ratings > h2:nth-child(1)|replace\\\"/[^\\d{4}]\\\":\\\"\\\"}}",
      "type": "number"
    },
    {
      "name": "available",
      "value": "true",
      "type": "checkbox"
    },
    {
      "name": "title",
      "value": "{{selector:#_automation_test_VDP_h1|replace:\\\"Used \\\":\\\"\\\"|safe_title}}",
      "type": "text"
    },
    {
      "name": "aliases",
      "value": "{{selector:#_automation_test_VDP_h1|replace:\\\"Used \\\":\\\"\\\"|safe_title}}",
      "type": "multitext"
    }
  ],
  "triggers": [
    "www.carfax.com/vehicle/"
  ],
  "noteNameFormat": "{{selector:div.vehicle-module__details|replace:\"VIN: \":\"\"|replace:\"/ · Stock #\\: .+/g\":\"\"|safe_name}}",
  "path": "Atlas/2025 Car Search"
}

3. Rank the car

Because there’s too much info to hold it all in my head at once, I then go to a canvas and put the car into one of three tiers. I also include a short “reason” why it is there so that I can remember in the future. After I have all of them in for a given search/day, I activate Semantic Canvas to save this data to the note properties for later querying.

4. Review the data

This query is written to show everything at a glance. The Meta-Bind embed is used to remove vehicles that are no longer available for sale. In this case, I needed to restart Obsidian for that car to be removed, but it is normally very responsive.

```datacorejsx
// A list of columns to show in the table.
const COLUMNS = [
	{
	    id: 'Name',
	    value: row =>{
		    return dc.coerce.link(`[[${row.$name}|${row.value('title')}]]`)
	    }
    },
    { id: 'Rating', value: page => page.value('rating') },
    { id: 'Price', value: page => page.value('listing-price') },
    { id: 'Diff.', value: page => page.value('difference') },
    { 
	    id: 'Link', 
	    value: row => {
		    const field = 'source'
		    const link = row.value(field)
		    const site = String(link)
		    return `[link](${site})`
		} 
	},
    { id: 'Dealer', value: page => page.value('dealer') },
	{
		id: "Cover",
		value: row => {
		  /* User Config */
		  const field = "cover"
		  const width = 100
		  const alt = ""
		  
		  /* Logic */
		  const link = row.value(field)
		  const image = String(link)
		  const metadata = `|${alt}|${width}`
		
		  if (image.startsWith('http')) {
			return `![${metadata}](${image})`
		  } else {
			return `![[${link.path}${metadata}]]`
		  }
		}
    },
    { id: 'Avail.', value: page => '`INPUT[toggle():' + page.$path + '#available]`' }
];

return function View() {
    // Selecting `#game` pages, for example.
    const pages = dc.useQuery('@page and #car/2025 and (available = "true" or available = true) and location.contains("Columbia")');

    // Uses the built in 'vanilla table' component for showing objects in a table!
    return <dc.Table columns={COLUMNS} rows={pages} />;
}
```

5. Dealerships

This one is pretty janky but it does a number of things:

  1. A datacore query that lists the cars available at that dealership, the tier of the car, the reason for it, and a Meta-Bind embed so that I can easily list any thoughts that I have after going to see it
  2. A number of inline DataVIEW functions (no inline DataCore yet) that breaks the cars down in various ways. This is then stored in the frontmatter using Dataview (to) Properties with “_” prepended to them. Cars is a list in the settings, but for A B and C I had to manually append the _list setting.
  3. Interactions with Leaflet
    1. Coordinates for the dealership taken from OpenStreetView
    2. A Meta-Bind function that saves the number of cars available as text to mapmarker. More on this in a moment. But this is the cleanest way I’ve found to do it, because you need to do math to access .length but it needs to be put out as text to be parsed by Leaflet. So make sure this property is parsed as text in Obsidian.

---
location:
  - ""
  - ""
address: 1200 Anywhere Dr, City, ST 12345
tags:
  - dealership
mapmarker: 7
...specifics about cars...
---

```datacorejsx
// A list of columns to show in the table.
const COLUMNS = [
	{
	    id: 'Name',
	    value: row =>{
		    return dc.coerce.link(`[[${row.$name}|${row.value('title')}]]`)
	    }
    },
    { id: 'Tier', value: page => page.value('tier') },
    { id: 'Reason', value: page => page.value('reason') },
    { id: 'Thoughts', value: page => '`INPUT[textArea():' + page.$path + '#thoughts]`' }
];

return function View() {
    // Selecting `#game` pages, for example.
    const pages = dc.useQuery('@page and #car/2025 and dealer = [[Joe Machens Volkswagen]] and available = true');
    const tasks = dc.useQuery('@task and childof(@page and #car/2025 and dealer = [[Joe Machens Volkswagen]])')

    // Uses the built in 'vanilla table' component for showing objects in a table!
    return <dc.Table columns={COLUMNS} rows={pages} />;
}
```

cars:: `$=dv.pages('#car/2025').where(p => dv.func.contains(p.dealer,dv.current().file.link)).where(p => dv.func.contains(p.available,true)).file.link`

carslen:: `$=dv.pages('#car/2025').where(p => dv.func.contains(p.dealer,dv.current().file.link)).where(p => dv.func.contains(p.available,true)).file.link.length`

Alen:: `$=dv.pages('#car/2025').where(p => dv.func.contains(p.dealer,dv.current().file.link)).where(p => dv.func.contains(p.tier,"A")).file.link.length`

Blen:: `$=dv.pages('#car/2025').where(p => dv.func.contains(p.dealer,dv.current().file.link)).where(p => dv.func.contains(p.tier,"B")).file.link.length`

Clen:: `$=dv.pages('#car/2025').where(p => dv.func.contains(p.dealer,dv.current().file.link)).where(p => dv.func.contains(p.tier,"C")).file.link.length`


A_list:: `$=dv.pages('#car/2025').where(p => dv.func.contains(p.dealer,dv.current().file.link)).where(p => dv.func.contains(p.tier,"A")).file.link`

B_list:: `$=dv.pages('#car/2025').where(p => dv.func.contains(p.dealer,dv.current().file.link)).where(p => dv.func.contains(p.tier,"B")).file.link`

C_list:: `$=dv.pages('#car/2025').where(p => dv.func.contains(p.dealer,dv.current().file.link)).where(p => dv.func.contains(p.tier,"C")).file.link`

```meta-bind-js-view
{_cars} as var1
save to {mapmarker}
---
return `${context.bound.var1.length}`;
```

6. Map it out

This is the page that lays it out so that I can actually see it and make a plan.

  1. Leaflet map! I made custom map pins that are not pretty but labeled 0-9 for the number of cars that are there. It’s updated based on the queries on the dealership notes and stored there with the help of that Meta-Bind function
  2. A Datacore query that is also grouped by dealership, but lists the cars as well as a list of how many of each tier are at a given dealership, just to break it down a different way.

```leaflet
id: leaflet-map
OpenStreetMap: [https://www.openstreetmap.org/?#map=12/0/0](https://www.openstreetmap.org/?#map=12/0/0)
lat: 0
long 0
minZoom: 1
maxZoom: 20
defaultZoom: 12
unit: miles
scale: 1
marker: home, 0,0, Home
markerFolder: Atlas/2025 Car Search/dealerships
darkMode: true
```

// Replace the zeros with your own coordinates

```datacorejsx
// A list of columns to show in the table.
const COLUMNS = [
    { id: 'Name', value: page => page.$link },
    { 
	    id: 'Cars', 
	    value: page => "A: " + page.value('_Alen') + "; B: " + page.value('_Blen') + "; C: " + page.value('_Clen') 
	},
    { id: 'Cars', value: page => page.value('_cars') }
];

return function View() {
    // Selecting `#game` pages, for example.
    const pages = dc.useQuery('@page and #dealership and !path("Atlas/Utility") and exists("_cars")');

    // Uses the built in 'vanilla table' component for showing objects in a table!
    return <dc.Table columns={COLUMNS} rows={pages} />;
}
```

7. Make your plan

Now there’s a copious amount of information that can be used to plan your approach. Good luck and happy hunting!

Plugins

Core

Web Clipper

Community

  1. Semantic Canvas
  2. Dataview
  3. Meta-Bind
  4. JS Engine (to use the code block for leaflet marking)
  5. Leaflet
  6. BRAT (to install the next two)
    1. Datacore
    2. Dataview (to) Properties

Other

Theme: Minimal
Modular CSS Layout
Leaflet pins

2 Likes