Templater User Scripts
for Obsidian
🏠 Page

🚀 prompt

This script, prompts the user for the relevant template variables based on a configuration object. It also handles incorrect template configuration, cancellation events, and other errors. The script is intended to be used as a user script for the Templater plugin.

The following provides an overview of the topics covered on this page:

Usage

tp.user.prompt(tp, config)

Arguments

The following arguments, in the presented order, are required:

tp

The Templater object passed from the context of a template.

config

The configuration object that holds relevant template variables. Each template variable holds a single configuration object, that is composed of an arbitrary number of configuration elements objects. In turn, each configuration element can have some of the following properties:

{
    prompt: boolean,
    display: string,
    value: string | string[] | (tp, config) => string | (tp, config) => string[],
    multiple: boolean,
    limit: number,
    text: string[] | (item: any) => string,
    check: (value: string, tp: Templater, config: object) => boolean,
    process: (value: string, tp: Templater, config: object) => string
}

Regardless of how Templater is invoked, the prompt and value are required configuration element properties. If the prompt value is set to true, the display property is also required. All other properties mentioned above are optional. For example, a configuration object with the elements path and filename might look like this:

// The configuration object.
let config = {
    // The `path` element indicating where to create the note.
    path: {
        prompt: true,
        display: "Please indicate the path.",
        value: "Folder"
    },

    // The `filename` element indicating how to name the note.
    filename: {
        prompt: true,
        display: "Please indicate the file name.",
        value: "A new note"
    }
}

The three properties presented below represent required configuration element properties.

prompt

display

value

Aside from the prompt, display, and value properties above, the following optional properties can also be used.

multiple

limit

text

check

process

As a rule, the following order is followed when evaluating the configuration elements:

Value

The prompt function call returns undefined. The configuration object is passed by reference and modified in-place.

Examples

In this section, we succinctly review some of the capabilities of the prompt user script in the context of another user script called makeNoteWithPrompting, which uses prompt behind the scenes to create a note file, or insert a template into an existing note. Please refer back to the Usage section for more information on the configuration elements and their properties. We will gradually build up towards a fully-fledged fictive template for creating questionably helpful movie notes.

Inserting template

Suppose we have a movie template creatively called Movie Template with the following content:

<%-*
// The configuration object containing several elements.
let config = {
    // The movie title.
    title: {
        prompt: true,
        display: "What is the tile of the movie?",
        value: "The Movie Title"
    },

    // The genre of the movie.
    genre: {
        prompt: true,
        display: "What is the movie genre?",
        value: ["comedy", "thriller", "other"]
    },

    // Whether the movie is re-watchable.
    rewatch: {
        prompt: true,
        display: "Would I rewatch this movie?",
        value: ["yes", "no"],
        text: ["Without a doubt, yes!", "I mean... it was okay, but..."]
    },

    // A general summary of the movie.
    summary: {
        prompt: true,
        display: "What is the summary of the movie?",
        value: "Summary...",
        multiline: true
    }
}

// Create the note.
await tp.user.makeNoteWithPrompting(tp, config)
_%>

# 🎥 <% config.title.value %>

## Description

- Genre: #<% config.genre.value %>
- Re-watchable: <% config.rewatch.value %>

## Summary

<% config.summary.value %>

Running the Templater: Open insert Template modal command from within an open note and selecting the Movie Template template will prompt the user for all the config object elements that have prompt set to true. Then, depending on the input provided, the following will be inserted in the open note:

# 🎥 A cool movie title

## Description

- Genre: #other
- Re-watchable: no

## Summary

Line one
Line two

Creating note from template

Continuing with the template above, suppose we want to be able to use the sample template to also create new notes from it. We can easily adjust our configuration object to support this use case by specifying the path and filename properties. After this change, our configuration object might look like this:

// The configuration object containing several elements.
let config = {
    // The note location.
    path: {
        prompt: true,
        display: "Where to store the movie note?",
        value: "Movies"
    },

    // The note file name.
    filename: {
        prompt: true,
        display: "How to name the movie note?",
        value: "New Movie"
    },

    // The movie title.
    title: {
        prompt: true,
        display: "What is the tile of the movie?",
        value: "The Movie Title"
    },

    // The genre of the movie.
    genre: {
        prompt: true,
        display: "What is the movie genre?",
        value: ["comedy", "thriller", "other"]
    },

    // Whether the movie is re-watchable.
    rewatch: {
        prompt: true,
        display: "Would I rewatch this movie?",
        value: ["yes", "no"],
        text: ["Without a doubt, yes!", "I mean... it was okay, but..."]
    },

    // A general summary of the movie.
    summary: {
        prompt: true,
        display: "What is the summary of the movie?",
        value: "Summary...",
        multiline: true
    }
}

Note. The rest of the template remains unchanged, we only adjusted the config object by adding the path and filename elements.

Running the Templater: Create new note from template command after the adjustments, we will also be prompted for the path and filename elements. Based on the input provided for these two elements, a new note will be created with the specified name, in the location indicated. The contents of the new note will be the same as the one shown in the previous example.

Value referencing

Let’s continue building on the template from the previous example and see how we can use referencing to make our template more dynamic. Suppose we want the title of the movie to set as the file name of the note. We can easily do this by using the {{ ... }} syntax to reference the title element in the filename. To achieve this, we need to change our config object as follows:

// The configuration object containing several elements.
let config = {
    // ...

    // The note file name.
    filename: {
        prompt: true,
        display: "How to name the movie note?",
        value: "{{ title }}"
    },

    // The movie title.
    title: {
        prompt: true,
        display: "What is the tile of the movie?",
        value: "The Movie Title"
    },

    // ...
}

Invoking Templater after this change will result in setting the value of filename to whatever the value of title is set to. Since our example also uses prompting, the prompt modal for filename will be populated with the value of title as the default value.

Note. The title element appears after filename in the config object. This is not issue because the the script will first handle elements that do not relay on referencing, then, it will sort the elements that use referencing and handle them in the correct order.

Value Checking

Building on the example above, let’s continue by adding a validation check to ensure that that the thriller option is not a valid choice of movie genre. We can easily accomplish this by adding a check property to the genre configuration element.

The check property must be a function that takes the value property as input and returns true if the value is considered valid, and false otherwise. In our example, the check property might look like this:

// The configuration object containing several elements.
let config = {
    // ...

    // The genre of the movie.
    genre: {
        prompt: true,
        display: "What is the movie genre?",
        value: ["comedy", "thriller", "other"],
        check: (value) => value === "thriller" ? false : true
    },


    // ...
}

Now, running Templater and selecting thriller as the genre will result in an error message saying that the value is not valid, and the script will stop executing.

Note. The check property can also specify a function with the tp and config parameters should more complex validation be required, e.g.:

// ...

check: (value, tp, config) => {
    // Do something with the `config` or `tp` objects here.

    // Return `true` or `false` based on the validation.
}

// ...

Value Processing

Finally, let’s see how to use value processing to modify the value property of a config element and re-prompt the user with the modified value. To achieve this, we need to add a process property. The process property must be a function that takes the value property as input and returns a string representing the modified value.

Suppose we want the summary element to reference the movie title, which in turn will be processed by a custom function to obtain the movie summary from an external API. In a nutshell, the following will happen:

Our custom processing function is called getMovieSummary and takes as input the title of a movie and returns its summary, e.g., using the OpenAI API, as seen below.

// Get a movie summary based on its title.
async function getMovieSummary(prompt, systemPrompt = "Your role is to identify a movie by its title and provide a succinct summary.") {
    // The OpenAI API endpoint.
    const openaiURL = "https://api.openai.com/v1/chat/completions";

    // The OpenAI API request headers.
    const headers = {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`
    };

    // The OpenAI API request body.
    const body = JSON.stringify({
        model: "gpt-3.5-turbo",
        messages: [
            { role: "system", content: systemPrompt },
            { role: "user", content: prompt }
        ]
    });

    // Send the request to the OpenAI API.
    const response = await fetch(openaiURL, {
        method: "POST",
        headers: headers,
        body: body
    });

    // Parse the response object.
    const data = await response.json();

    // Get the assistant response.
    const assistantResponse = data.choices[0].message.content;

    // Return the response.
    return assistantResponse;
}

Note. The function getMovieSummary assumes that your OpenAI API key is stored in the OPENAI_API_KEY environment variable and is available system-wide (e.g., via launchctl setenv for macOS). If you do not know how to achieve this, just replace ${process.env.OPENAI_API_KEY} in the function with your actual API key.

Now that we have our custom processing function, we can place it at the top of our template file and then add it to the process property on the summary configuration element. The config object might look like this:

// The configuration object containing several elements.
let config = {
    // ...

    // A general summary of the movie.
    summary: {
        prompt: true,
        display: "What is the summary of the movie?",
        value: "{{ title }}",
        multiline: true,
        process: getMovieSummary
    }

    // ...
}

Note. It might take a few seconds for the API call to complete. Once the summary is available, the user will be prompted again with the chance to edit it. If you would like to avoid the confirmation prompt, you can leverage the additional arguments, by updating the process property to the following:

// ...

process: async (value, tp, config) => {
    // Disable the confirmation prompt.
    config.summary.prompt = false

    // Process the processed value (i.e., the summary).
    return await getMovieSummary(value)
}

// ...

Error handling

It is advisable that you wrap the makeNoteWithPrompting script in a try ... catch block to gracefully handle errors. For example, you may consider replacing the lines

// Create the note.
await tp.user.makeNoteWithPrompting(tp, config)

with

// Proceed with the note creation gracefully.
try {
    // Create the note or insert the template.
    await tp.user.makeNoteWithPrompting(tp, config)

    // Let the user know if the operation succeeded.
    new Notice(`Created note '${config.filename.value}'.`)
} catch(error) {
    // Inform the user.
    new Notice(error.message)

    // Stop the execution.
    return
}

This will allow errors (e.g., failures if the note already exists, prompt cancellations, failed checks etc.) to be caught and displayed in the Obsidian interface as gentle notices.

The complete template

Let’s put everything together and see how the complete template looks like:

<%-*
// Get a movie summary based on its title.
async function getMovieSummary(prompt, systemPrompt = "Your role is to identify a movie by its title and provide a succinct summary.") {
    // The OpenAI API endpoint.
    const openAiURL = "https://api.openai.com/v1/chat/completions";

    // The OpenAI API request headers.
    const headers = {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`
    };

    // The OpenAI API request body.
    const body = JSON.stringify({
        model: "gpt-3.5-turbo",
        messages: [
            { role: "system", content: systemPrompt },
            { role: "user", content: prompt }
        ]
    });

    // Send the request to the OpenAI API.
    const response = await fetch(openAiURL, {
        method: "POST",
        headers: headers,
        body: body
    });

    // Parse the response object.
    const data = await response.json();

    // Get the assistant response.
    const assistantResponse = data.choices[0].message.content;

    // Return the response.
    return assistantResponse;
}

// The configuration object containing several elements.
let config = {
    // The note location.
    path: {
        prompt: true,
        display: "Where to store the movie note?",
        value: "Movies"
    },

    // The note file name.
    filename: {
        prompt: true,
        display: "How to name the movie note?",
        value: "{{ title }}"
    },

    // The movie title.
    title: {
        prompt: true,
        display: "What is the tile of the movie?",
        value: "The Movie Title"
    },

    // The genre of the movie.
    genre: {
        prompt: true,
        display: "What is the movie genre?",
        value: ["comedy", "thriller", "other"],
        check: (value) => value === "thriller" ? false : true
    },

    // Whether the movie is re-watchable.
    rewatch: {
        prompt: true,
        display: "Would I rewatch this movie?",
        value: ["yes", "no"],
        text: ["Without a doubt, yes!", "I mean... it was okay, but..."]
    },

    // A general summary of the movie.
    summary: {
        prompt: true,
        display: "What is the summary of the movie?",
        value: "{{ title }}",
        multiline: true,
        process: getMovieSummary
    }
}

// Proceed with the note creation gracefully.
try {
    // Create the note or insert the template.
    await tp.user.makeNoteWithPrompting(tp, config)

    // Let the user know if the operation succeeded.
    new Notice(`Created note '${config.filename.value}'.`)
} catch(error) {
    // Inform the user.
    new Notice(error.message)

    // Stop the execution.
    return
}
_%>

# 🎥 <% config.title.value %>

## Description

- Genre: #<% config.genre.value %>
- Re-watchable: <% config.rewatch.value %>

## Summary

<% config.summary.value %>

The following video shows the template above in action: