Tag Propagator — inherit tags from your first note to related notes in one click

Tag Propagator — inherit tags from your first note to related notes in one click

The problem
You're writing a series of notes on the same topic. The first one gets properly tagged. The next five? You were in flow — tagging was the last thing on your mind.

Joplin already lets you select multiple notes and assign tags to all of them at once. But that workflow assumes you already have a clear overview: you know which notes are untagged, you select them all, and then you act. In practice, that moment of overview rarely comes naturally.

What this plugin does differently
Tag Propagator works the other way around. Select a group of related notes — the first one should have the tags — right-click and choose "Inherit tags from first note". Any note in your selection that has no tags yet will instantly receive the tags from the first note. Notes that already have their own tags are left completely untouched.

That's it. No dialogs to fill in, no tag picker, no overhead.

How to trigger it

  • Right-click on a note selection → Inherit tags from first note
  • Menu bar → Note → Inherit tags from first note
  • Keyboard shortcut: Ctrl+Shift+T (Mac: Cmd+Shift+T)

Important: select the tagged note first
Hold Ctrl and click the note with the tags first, then click the other notes. The plugin always reads the tags from the first note in your selection.

Installation
Download the attached tag-propagator-1.0.1.jpl and install via:
Tools → Options → Plugins → gear icon → Install from file

Source code

/* Tag Propagator v1.0.1 */
(()=>{
"use strict";

joplin.plugins.register({
    onStart: async function() {

        await joplin.commands.register({
            name: 'tagPropagatorRun',
            label: 'Inherit tags from first note',
            execute: async () => {
                await propagateTags();
            },
        });

        await joplin.views.menuItems.create(
            'tagPropagatorContextMenu',
            'tagPropagatorRun',
            'noteListContextMenu',
            { accelerator: 'CmdOrCtrl+Shift+T' }
        );

        await joplin.views.menuItems.create(
            'tagPropagatorNoteMenu',
            'tagPropagatorRun',
            'note'
        );
    },
});

async function propagateTags() {
    const noteIds = await joplin.workspace.selectedNoteIds();

    if (!noteIds || noteIds.length === 0) {
        await joplin.views.dialogs.showMessageBox(
            'No notes selected.\n\nPlease select 2 or more notes and try again.'
        );
        return;
    }

    if (noteIds.length < 2) {
        await joplin.views.dialogs.showMessageBox(
            'Only 1 note selected.\n\nPlease select at least 2 notes. The first note you select should be the one with the tags you want to copy.'
        );
        return;
    }

    const firstNote = await joplin.data.get(['notes', noteIds[0]], { fields: ['id', 'title'] });
    const firstNoteTags = await getTagsForNote(noteIds[0]);

    if (firstNoteTags.length === 0) {
        await joplin.views.dialogs.showMessageBox(
            'The first selected note "' + firstNote.title + '" has no tags.\n\nMake sure the note with the tags is the first one you select, then select the other notes while holding Ctrl.'
        );
        return;
    }

    let updatedCount = 0;
    let skippedCount = 0;

    for (let i = 1; i < noteIds.length; i++) {
        const noteTags = await getTagsForNote(noteIds[i]);
        if (noteTags.length === 0) {
            for (const tag of firstNoteTags) {
                await joplin.data.post(['tags', tag.id, 'notes'], null, { id: noteIds[i] });
            }
            updatedCount++;
        } else {
            skippedCount++;
        }
    }

    const tagNames = firstNoteTags.map(function(t) { return t.title; }).join(', ');
    let msg = 'Done!\n\nTags applied: "' + tagNames + '"\n\n' + updatedCount + ' note(s) updated.';
    if (skippedCount > 0) msg += '\n' + skippedCount + ' note(s) skipped (already had tags).';

    await joplin.views.dialogs.showMessageBox(msg);
}

async function getTagsForNote(noteId) {
    const tags = [];
    let page = 1;
    while (true) {
        const result = await joplin.data.get(['notes', noteId, 'tags'], { page: page });
        if (result && result.items) {
            for (const item of result.items) tags.push(item);
        }
        if (!result || !result.has_more) break;
        page++;
    }
    return tags;
}

})();

— wnen

tag-propagator-1.0.1.jpl (10 KB)