When I first set up my Zotero-to-Obsidian workflow, I thought attaching images from Zotero annotations could not be done with a normal template, so I wrote a user script. However, right before publishing the main article, I found a simple template-based method on Reddit—so this user script turned out to be unnecessary.
Still, I’m leaving this here as a record in case a user script is useful for other purposes in the future.
A “user script” is JavaScript code that you can call from plugins such as Templater.
If you put the following code in your template, you can call a function named
insertImage
from your user script:
<%* tp.user.insertImage(tp) %>
The user script file itself is saved in the location configured in Templater (described later).
The folder structure is the same as in the main Obsidian+Zotero article:
Vault
├── Project 1
├── Literature
│ └── Inbox (literature notes are saved here)
├── Templates (templates are saved here)
│ └── Scripts (scripts for attaching images)
└── Attachments (images are saved here)
Zotero Integration settings are also the same as in the main article:
Import Format
Name (name shown in the command palette; any name is fine)
Import note from zotero
Output Path (where notes are saved + filename format)
Literature/Inbox/{{date | format("YYYY-MM")}} {{creators[0].lastName}} {{creators[0].firstName[0]}}.md
Image Output Path (where extracted images are saved)
Attachments/{{date | format("YYYY-MM")}} {{creators[0].lastName}} {{creators[0].firstName[0]}}/
Template (template file)
Template/Zotero_Template.md
Below is the template. Copy it, save it as
Zotero_Template.md
, and place it in the
Templates
folder.
The last line includes the code that calls the user script.
---
category: literaturenote
tags: {% if allTags %}{{allTags}}{% endif %}
citekey: {{citekey}}
rating:
---
*{{publicationTitle}}*
{{title}}
# Summary
> [!My note]
> Synopsis::
# Methods
# Results
## PDF
{%- for attachment in attachments | filterby("path", "endswith", ".pdf") %}
[{{attachment.title}}](file://{{attachment.path | replace(" ", "%20")}}) {%- endfor -%}.
## Abstract
{%- if abstractNote %}
{{abstractNote}}
{%- endif -%}.
## Metadata
{% for type, creators in creators | groupby("creatorType") -%}
{%- for creator in creators -%}
**{{"First" if loop.first}}{{type | capitalize}}**::
{%- if creator.name %} {{creator.name}}
{%- else %} {{creator.lastName}}, {{creator.firstName}}
{%- endif %}
{% endfor %}
{%- endfor %}
**Title**:: {{title}}
**Year**:: {{date | format("YYYY-MM")}}
**Citekey**:: {{citekey}} {%- if itemType %}
**itemType**:: {{itemType }}{%- endif %}{%- if itemType == "journalArticle" %}
**Journal**:: *{{publicationTitle}}* {%- endif %}{%- if volume %}
**Volume**:: {{volume}} {%- endif %}{%- if issue %}
**Issue**:: {{issue}} {%- endif %}{%- if itemType == "bookSection" %}
**Book**:: {{publicationTitle}} {%- endif %}{%- if publisher %}
**Publisher**:: {{publisher}} {%- endif %}{%- if place %}
**Location**:: {{place}} {%- endif %}{%- if pages %}
**Pages**:: {{pages}} {%- endif %}{%- if DOI %}
**DOI**:: {{DOI}} {%- endif %}{%- if ISBN %}
**ISBN**:: {{ISBN}} {%- endif %}{%- if desktopURI %}
**DesktopURI**:: [zotero-link]({{desktopURI}}){%- endif %}
> **Related**:: {% for relation in relations | selectattr("citekey") %} [[@{{relation.citekey}}]]{% if not loop.last %}, {% endif%} {% endfor %}
> {%- if markdownNotes %}
> {{markdownNotes}}{%- endif -%}.
<%* tp.user.insertImage(tp) %>
Configure Templater like this:
Template folder location:
Templates
User script functions
Script files folder location:
Templates/Scripts
Below is the script that appends images to the literature note. Copy it, save it as
insertImage.js
, and place it in
Templates/Scripts
. I recommend doing this in Finder/Explorer rather than inside Obsidian, because Obsidian may hide non-md / non-image files.
async function insertImages(tp) {
const files = app.vault.getFiles();
let imageLinks = '';
files.forEach(file => {
if (file.path.includes(tp.file.title) && file.path.endsWith(".png")) {
console.log(file.path);
const imageLink = `![[${file.path}]]\n`;
imageLinks += imageLink;
console.log(imageLinks);
}
if (file.path.includes(tp.file.title)
&& file.path.endsWith(".md")
&& file.path.includes("Inbox")) {
console.log(file.path);
mdFile = file;
}
});
if (mdFile) {
const fileContent = await app.vault.read(mdFile);
await app.vault.modify(mdFile, fileContent + imageLinks);
console.log("Image Inserted.");
} else {
console.error("Markdown file not found.");
}
}
module.exports = insertImages;
Go back to Templater settings and confirm it shows something like "Detected 1 user script" under User script functions.
Importing Zotero literature notes into Obsidian is done from the command palette, as described in the main article.
I originally implemented this using a user script because I couldn’t figure out how to attach imported images directly in the note using a normal template. Then, the next day, I found a Reddit post showing the template-based approach—so the user script I spent time on ended up being obsolete.
Still, I’m keeping the user-script approach here for reference.
The main reason I got stuck was that the following code didn’t work as expected. Even after asking Copilot, Gemini, and ChatGPT, I couldn’t get it working and spent a lot of time on it.
const files = app.vault.getAbstractFileByPath('path_to_folder')
When importing a Zotero note, images are saved to a specific folder—but you don’t know the filenames ahead of time. I expected
getAbstractFileByPath()
to return files under
path_to_folder
, but it kept returning
null
.
So instead, I used
getFiles()
to list all files in the vault and then scanned them in a loop to find the ones that matched the folder path. It’s a bit of a hack, but it worked.
const files = app.vault.getFiles();
Last updated: March 22, 2025