Literature workflow with Obsidian + Zotero: User script (legacy approach)

Overview: how the user script works

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).

Zotero Integration settings

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) %>
Templater settings

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.

How to use Zotero Integration

Importing Zotero literature notes into Obsidian is done from the command palette, as described in the main article.

Notes: what I struggled with

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