Update Note Type A frontmatter based on Notes Type B frontmatter

What I’m trying to do

I have notes for classes I teach (‘type = class_instance’) and notes for students who have attended classes (‘type = student’). The student notes have a frontmatter field ‘classes_attended’ where I manually add the ‘class_instance_id’ value of each class they’ve attended (I’m open to tweaking this part of the workflow if needed).

I’m attempting to create a process (auto-updating or one-time check when opening the note, but a button would work too) for the ‘class_instance’ notes that would:

  1. Read the current note’s ‘class_instance_id’ from frontmatter.
  2. Scan all notes with ‘type: student’.
  3. Find the student notes whose ‘classes_taken’ field includes the current note’s ‘class_instance_id’ (‘classes_taken’ field may also include other ‘class_instance_id’ values if they’ve taken multiple classes, this field is structured as a list).
  4. Collect those students’ ‘student_id’ values (or note titles) and write them as separate entries into the current note’s ‘students_attended’ (another list) frontmatter.
  5. (Optional, but would really like to have, maybe a separate question) Add up the number of matching student notes and write the number into the field ‘number_of_students’.

Example Class note frontmatter:

---
type: class_instance
class_instance_id: CI_2025-01-01
students_attended:
number_of_students: 0
---

Example Student note frontmatter:

---
type: student
student_id: S0001
classes_taken:
  - CI_2025-01-01
  - CI_2025-03-01
---

Things I have tried

I’ve searched community plugins and this forum, I’ve asked ChatGPT, I’ve tried using Templater, Metabind, and some JS but everything has given me errors and I’m stuck. If this is possible, what is the best way to go about it? What plugins do I need, what methods should I use? I really appreciate any help you can give me!

Taken together:

  • filter files by a specific property
  • compare this property to one in the current note
  • modify properties
  • append content to the note

All of these steps can be handled by a Templater script using JS and the Obsidian API, though it does require some effort. This might be helpful: Templater snippets.

Another option is to use the Bases plugin. You can embed a base for each class and filter all students attending that class by using the ID. You may need to slightly adjust your workflow, but Bases should do this well. I’m doing something similar with cross-referencing a storage system via IDs, and it works.

2 Likes

Thanks for your reply! I’ll take a look at the Templater snippets and see if I can figure it out.

Re: bases - You’re just saying insert a base to display the students in that class, not that bases can somehow populate the frontmatter, right?

Both. Bases can make use of recursive formulas using this. to access properties of the notes they are embedded in. So you would create a base and filter it for students who have file.properties.classes_taken.contains(this.class_instance_id) (I did not test it, so maybe not 100% correct syntax, just as an idea). When you embed this base in any of your class notes, it will give you a view showing all students who attended that class. This provides an automatic overview of what you want, as far as I understand.

In Bases table view you can also edit frontmatter properties. With multi-row editing, including copying and pasting columns, you should be able to quickly add a taken class to multiple students at once.

1 Like

Thank you! This code block in the template file makes that bases view work whenever I create a new class note:

views:
  - type: table
    name: Class Roster
    filters:
      and:
        - note.type == "student"
        - classes_taken.contains(this.class_instance_id)
    order:
      - student_first_name
      - student_last_name
      - student_pronouns
      - classes_taken
      - file.name

And in case this helps someone else, I got the other part working too. My student notes have a ‘classes_taken’ field. I manually fill that in as students sign up. My class notes have a ‘students_attended’ field and a ‘number_of_students’ field. I made a QuickAdd macro (and then a meta-bind button to run it) that updates those two fields in the class note based on which students have taken that class. Here’s the js:

module.exports = async (params) => {
  const { app } = params;

  // Get the current file and its frontmatter
  const file = app.workspace.getActiveFile();
  const cache = app.metadataCache.getFileCache(file);
  const classId = cache?.frontmatter?.class_instance_id;

  // Exit if no class_instance_id is found
  if (!classId) {
    new Notice("No class_instance_id found");
    return;
  }

  // Find all student files by type
  const studentFiles = app.vault.getMarkdownFiles().filter((f) => {
    const cache = app.metadataCache.getFileCache(f);
    return cache?.frontmatter?.type === "student";
  });

  const students = [];

  // Loop through each student file
  for (const studentFile of studentFiles) {
    const studentCache = app.metadataCache.getFileCache(studentFile);

    // Check if this student has taken this class
    if (studentCache?.frontmatter?.classes_taken?.includes(classId)) {
      // Add the student's filename to the list
      students.push(studentFile.basename);
    }
  }

  // Read the current file's content
  let content = await app.vault.read(file);
  const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
  const match = content.match(frontmatterRegex);

  if (match) {
    let frontmatter = match[1];

    // Prepare the students list in YAML list format
    const studentsList =
      students.length > 0
        ? "\n" + students.map((s) => `  - ${s}`).join("\n")
        : " []";

    // Count of students
    const studentCount = students.length;

    // Check if students_attended already exists
    if (/^students_attended:/m.test(frontmatter)) {
      // Replace existing field in place (preserves position)
      // This regex matches the field and all following indented lines
      frontmatter = frontmatter.replace(
        /^students_attended:.*$(\n  -.*$)*/gm,
        `students_attended:${studentsList}`,
      );
    } else {
      // Add new field after materials_version if it exists
      if (/^materials_version:/m.test(frontmatter)) {
        frontmatter = frontmatter.replace(
          /^materials_version:.*$/m,
          `$&\nstudents_attended:${studentsList}`,
        );
      } else {
        // Otherwise add at the end
        frontmatter += `\nstudents_attended:${studentsList}`;
      }
    }

    // Update number_of_students field
    if (/^number_of_students:/m.test(frontmatter)) {
      // Replace existing number in place (preserves position)
      frontmatter = frontmatter.replace(
        /^number_of_students:.*$/m,
        `number_of_students: ${studentCount}`,
      );
    } else {
      // Add new field after students_attended
      frontmatter = frontmatter.replace(
        /^students_attended:.*$(\n  -.*$)*/gm,
        `$&\nnumber_of_students: ${studentCount}`,
      );
    }

    // Replace the frontmatter in the content
    content = content.replace(frontmatterRegex, `---\n${frontmatter}\n---`);

    // Write the updated content back to the file
    await app.vault.modify(file, content);

    // Show success notification
    new Notice(`✅ Updated: ${students.length} students`);
  }
};