External JS Library on a CustomJS callback

I’ve been trying to import an external JS library to use it on a CustomJS callback, but it never gets scope in the callback.

As I was reading answers on the following thread, I assumed that the external JS is imported with a script tag on the head of the HTML template file, but in my case it never recognizes the library I import.

I dont know if its relative but my structure is like this:

± - init.py
± - main.py ← CustomJS triggers from a js_ on_change and executes download.js
± - static
----- | ± - js
---------- | ± - download.js ← js script thats running on the callback
±- templates
----- | ±- index.html ← where i import the library

@mateusz can you offer any guidance?

@steftsotras not sure what is not working since you have not supplied any code. But the following for me works where I load jQuery in templates/index.html. I have added a check in download.js that jQuery is loaded. But I do not know what you mean by external JS: external from a CDN or installed locally?

├── main.py
├── salary_data.csv
├── static/js
│   └── download.js
├── templates
│   └── index.html


''' A column salary chart with minimum and maximum values.
This example shows the capability of exporting a csv file from ColumnDataSource.
from os.path import dirname, join

import pandas as pd

from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import (Button, ColumnDataSource, CustomJS, DataTable,
                          NumberFormatter, RangeSlider, TableColumn)

df = pd.read_csv(join(dirname(__file__), 'salary_data.csv'))

source = ColumnDataSource(data=dict())

def update():
    current = df[(df['salary'] >= slider.value[0]) & (df['salary'] <= slider.value[1])].dropna()
    source.data = {
        'name'             : current.name,
        'salary'           : current.salary,
        'years_experience' : current.years_experience,

slider = RangeSlider(title="Max Salary", start=10000, end=110000, value=(10000, 50000), step=1000, format="0,0")
slider.on_change('value', lambda attr, old, new: update())

button = Button(label="Download", button_type="success")
        args = dict(source=source),
        code = open(join(dirname(__file__), "static/js", "download.js")).read()

columns = [
    TableColumn(field="name", title="Employee Name"),
    TableColumn(field="salary", title="Income", formatter=NumberFormatter(format="$0,0.00")),
    TableColumn(field="years_experience", title="Experience (years)")

data_table = DataTable(source=source, columns=columns, width=800)

controls = column(slider, button)

curdoc().add_root(row(controls, data_table))
curdoc().title = "Export CSV"



{% extends base %}
{% block preamble %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
{% endblock %}

{% block postamble %}
    h1 {
      margin: 2em 0 0 0;
      color: #2e484c;
      font-family: 'Julius Sans One', sans-serif;
      font-size: 1.4em;
      text-transform: uppercase;
    p {
      font: "Libre Baskerville", sans-serif;
      text-align: justify;
      text-justify: inter-word;
{% endblock %}

{% block contents %}
    <h1>Export data to CSV</h1>
    <p> This demo uses a <code>CustomJS</code> callback to export <code>DataTable</code> contents to CSV.
    <p><b>NOTE</b>: On Safari the CSV will open in a new tab, rather than downloading.</p>
    <ul style="list-style-type:circle">
      <li>Scrub the "Max Salary" slider and watch the <code>DataTable</code> change.</li>
      <li>Click the "Download" button to export data to a CSV file.</li>
  {{ super() }}
{% endblock %}


function table_to_csv(source) {
    const columns = Object.keys(source.data)
    const nrows = source.get_length()
    const lines = [columns.join(',')]

    for (let i = 0; i < nrows; i++) {
        let row = [];
        for (let j = 0; j < columns.length; j++) {
            const column = columns[j]
    return lines.join('\n').concat('\n')

const filename = 'data_result.csv'
const filetext = table_to_csv(source)
const blob = new Blob([filetext], { type: 'text/csv;charset=utf-8;' })

//addresses IE
if (navigator.msSaveBlob) {
    navigator.msSaveBlob(blob, filename)
} else {
    const link = document.createElement('a')
    link.href = URL.createObjectURL(blob)
    link.download = filename
    link.target = '_blank'
    link.style.visibility = 'hidden'
    link.dispatchEvent(new MouseEvent('click'))

if (typeof jQuery != 'undefined') {
    alert("jQuery library is loaded!");
    alert("jQuery library is not found!");

Thank you so much for the fast response,
I identified the problem which was just a typo, the folder “templates” had somehow been renamed to “template” which caused bokeh to not see index.html at all. Now that its fixed everything works as it should.

Thanks again, you can mark this as closed.