Learn Jam.py

By Nick Antonaccio
2-21-2025

Contents:

1. GETTING STARTED
1.1 What is Jam.py?
1.2 Some examples
2. JAM.PY FEATURES
2.1 No-code capabilities
2.2 Low-code
2.3 Pro-code
2.4 Browser-based toolchain
2.5 Other features and benefits
3. SOME VIDEO TUTORIALS
4. DOWNLOADABLE DEMO CODE
5. A TUTORIAL TO TEACH GENERATIVE AI TO USE JAM.PY
5.1 A copy-paste prompt to teach LLMs jam.py in-context
6. FINAL THOUGHTS

1. GETTING STARTED

1.1 What is Jam.py?

Jam.py is a uniquely productive full-stack web development framework.

Unlike other web frameworks, Jam.py generates portable SQL database schema, without having to write any code, and it automatically wires full-featured front-end UI grids, forms, navigation, and other essential CRUD componentry, all within a lightweight browser-based development environment.

Jam.py’s automated database/UI functionality is profoundly simple to use, even by people with zero software development experience. It is totally open source, usable on virtually any desktop, server, and/or mobile hardware/OS, and fully extensible with easy Python, Bootstrap and jQuery code, which enables the full power of the Python & HTML/CSS/JS ecosystems.

The videos below provide some introductory demonstrations of Jam.py:

https://www.youtube.com/watch?v=zcZBa8sf93M

https://www.youtube.com/watch?v=vY6FTdpABa4

https://www.youtube.com/watch?v=bhddYW0f3Sg

It’s a good idea to skim those videos at 1.75x speed before diving into Jam.py.

1.2 Some examples

The default Jam.py demo application is available here:

https://demo.jam-py.com

The ‘Northwind Traders’ demo below is an example by Dean Babic, which was migrated from an old MS Access application, in only 10 hours of total work, including all server installation/configuration, database schema creation, UI layout with fully interactive CRUD grids, forms, navigation, charts, printable report designs, and even a versatile pivot table interface:

https://northwind2.pythonanywhere.com

The application above contains links to many more example apps, with downloadable project files & source code for each complete application.

You can get started learning to install, run, and extend these examples, with all core database/UI functionality, in a single afternoon. The features you’ll read about next are broad, but they’re dead simple to learn how to use.

2. JAM.PY FEATURES

2.1 No-code capabilities

  1. Comprehensive no-code UI grid, form, and navigation generation, with automatically wired CRUD functionality, multi-column sorting, custom filters of any complexity, and responsive desktop/mobile UI layouts
  2. No-code database schema definition tools, with simple interfaces to create joins, foreign keys, master-detail tables, lookup lists, input masks, validations, default values, and other essential data organization features
  3. No-code connection to SQLite, Postgres, MSSQL, MySQL, Oracle, Firebase, and other RDBMSs, with automated import & manipulation of existing schema and data, from any supported database
  4. No-code user management, authentication & authorization controls, with fine-grained role-based permissions assignable to individual database tables.
  5. No-code audit trail generation (optional history of changes made to any database table, tracking of users who made each change, timestamps for every change, etc.)
  6. No-code soft delete features (optional removal of records from UI views, without permanently deleting database rows)
  7. No-code upload, download, display, and storage of images & files of any specified type (files are stored in the OS file system, so size and available manipulations aren’t constrained by database limitations)
  8. No-code adjustment of built-in themes, style properties, and layout parameters such as position, color, spacing, size, font selection, menu/grid customization, and other selectable UI design element attributes
  9. No-code multi-language support
  10. All no-code features are adjustable and extendable with custom code, whenever needed

2.2 Low-code

  1. Deeply capable low-code report generation features, including printing and output to PDF, CSV, XLS, ODF, HTML, and other formats. Even non-technical users can build report layouts simply, using a spreadsheet to implement visual designs (this requires only open source tools)
  2. A low-code integrated ORM for easy database queries, directly accessible in both back-end & front-end code (using #he same objects and API functions on both sides), eliminating the need for AJAX, REST APIs, SQL, JSON, and time-consuming data serialization, de-serialization and conversion between multiple programming language structures (as is often required in other full-stack frameworks)

2.3 Pro-code

  1. Full access to pure SQL code, as well as SQLAlchemy and any other Python database ORMs/drivers. This flexibility is especially useful when migrating existing code directly from legacy projects and prototype applications created with other frameworks.
  2. Full access to front-end HTML/CSS/JS/Bootstrap/jQuery tooling, and the ability to integrate any of the massive web-based front-end technology ecosystem. You’ll never experience front-end vendor lock-in or limitations imposed by Jam.py.
  3. Full access to back-end Python tooling and the massive Python library/framework ecosystem, which enables computing work in virtually any domain, including best-of-breed AI, data-science, visualization, document generation, hardware control, IoT, robotics, and other broad categories of computing technology (libraries such as Numpy, Pandas, Requests, Matplotlib, Seaborn, Tensorflow, PyTorch, Scikit-Learn, Plotly, Theano, Beautiful Soup, OpenCV, all the standard library, etc.). Connectivity with virtually every other sort of mainstream tooling, APIs, SDKs, etc., are almost always Python-first. You’ll never experience back-end vendor lock-in or limitations imposed by Jam.py.
  4. Easy creation of REST API endpoints, so applications written in other languages/frameworks can integrate via typical HTTP(S) protocol interactions

2.4 Browser-based toolchain

  1. One of Jam.py’s most important features is its set of browser-based code editors. These simple IDE tools enable creation and editing of all HTML, CSS, JavaScript, and Python code used in any project, directly in a web browser, without ever needing to use command line tools. Language-aware syntax checking, simple autocomplete, search/replace, and other features help to make the built-in IDE suitable as the sole toolkit required to complete entire projects.
  2. Jam.py also includes powerful browser-based project file management & migration tools (export and import entire projects with the press of a button).
  3. The framework integrates naturally with browser DevTools (the ‘F12’ console), to explore objects, to get/set data values, to set code execution breakpoints, etc., all within the browser, while an application is running live.
  4. Automatic live application updates occur whenever source code is changed.
  5. All together, these features enable Jam.py development work to be completed entirely in a web browser, on any common OS (Windows, Mac, Linux, Chromebook, Android, iOS, etc.), without any other client software or toolchain installation required. The entire Jam.py development environment, and the current state of any project, is instantly portable to any machine, desktop or mobile, simply by opening a URL in any browser.

2.5 Other features and benefits

  1. Jam.py is totally free and completely open source.
  2. Jam.py's integrated visual database tooling eliminates the need for 3rd party management applications such as DBeaver, HeidiSQL, SSMS, etc., to create/manage database schema, to view, sort, filter and edit data administratively, etc.
  3. Jam.py's IDE features integrate easily with other visual HTML builder tools, and standard Python/JavaScript generated by GPT, Claude, Gemini, Deepseek or any AI, can also be easily integrated into both the back-end and front-end modules of Jam.py. Tools such as Grapes.js (a popular open source browser-based HTML/CSS generation tool), for example, can be used to quickly create custom UI layouts for use in Jam.py, and the code output of any other similar WYSIWYG tool can typically be imported instantly.
  4. Jam.py's built-in no-code tools enable non-technical users to safely & easily create useful database schema and CRUD grid/form interfaces, with zero experience required. This opens up a whole new paradigm, in which users can reliably build/extend core parts of their own applications, without limiting the work of developers in any way. In addition to automatically generating many of the most critically useful capabilities required in software projects, the no-code workflows eliminate common syntax, logic, schema, query, or other errors. This not only reduces development time and effort, but also potential insidious mistakes, and testing time/work needed to ensure that data and user interactions are handled correctly.
  5. Jam.py can connect directly with SQLCipher, which encrypts single-file SQLite databases. This provides a genuinely simple solution for security compliance requirements in which data must be encrypted at rest, without any RDBMS server software needing to be installed/maintained. This feature alone can significantly reduce IT workloads.
  6. Jam.py's automated CRUD features enable developers to potentially eliminate and/or simplify tremendous portions of routine database and UI work, especially in projects with compliance obligations (HIPAA, PCI, etc.) that require implementing complex audit trail logging, soft delete, and auth solutions. Those sorts of requirements can be satisfied with truly zero additional work in Jam.py (that alone can save thousands of lines of code in large projects).
  7. As with any freeform web UI, Jam.py user interfaces can include iframes, which makes it a snap to integrate web applications written in *any other programming language/framework, simply by connecting to the same database and/or connecting REST APIs.
  8. All of Jam.py's features are encapsulated in a truly *miniscule, simple to use system, which requires very little memory and CPU power to run, and takes only a few moments to set up on virtually any hardware/OS with either Python 2 or 3 installed (virtually any Linux, Windows, or Mac machine). The server can even run comfortably on Chromebook, Android, iOS and other low-powered mobile platforms, old and new. Client devices simply require any mainstream browser to run Jam.py applications - no software ever needs to be installed for multiple users to execute and/or update Jam.py apps.
  9. Jam.py can be easily scaled by load-balancing servers and by employing other well established vertical and horizontal scaling techniques. Its small footprint, OS agnostic implementation, support for most common RDBMS systems, and WSGI support, provide many options and technical choices to fit virtually any deployment environment.

Jam.py’s deep capability is easy to miss at first glance, especially since its admin interface is so deceptively simple in appearance. Many professional developers also tend to look away instantly from any system associated with the words ’no-code’, but you will not find another comparable framework, which supports the same unique features to so effectively reduce software development complexity.

Take a look through a few examples in this tutorial, and you’ll quickly see how Jam.py not only eliminates common installation, configuration, maintenance, and toolchain drudgery, but also radically speeds up time-consuming CRUD database development activities - which often form a majority of the work involved in completing projects of all sizes.

For CRUD work, Jam.py’s productivity gains compare fantastically against those of any other mature web development framework. Jam.py enables impressive no-code tactics - practically integrated with the most popular, well-entrenched, fully extensible mainstream web development tools - to form a system which is simultaneously hyper-productive and usable even by non-coders, and capable of completing complicated production work, comprised of even the most complex and specifically detailed front-end & back-end coding requirements.

Jam.py enables you to spend time building the innovative features of applications, instead of basic CRUD - all in a way that’s surprisingly powerful and fun to use.

3. SOME VIDEO TUTORIALS

The intro video below covers basic topics about using Jam.py, including a brief but complete installation demonstration, all the basics about using Jam.py's no-code UI grid, schema generation, and auth features, and the basics of using its Python and JS code interfaces, to get started with freeform front-end and back-end development:

https://www.youtube.com/watch?v=ZGP6P8zdxek

This video overview demonstrates how to combine Jam.py with other web frameworks, using iframes, simply by connecting to the same database. The Jam.py+SQLPage+RShiny example at http://appstagehost.com:8080/persons.sql is demonstrated particularly:

https://www.youtube.com/watch?v=By-MHjqhpFw

This video covers how AI can be used to generate generic Python and JS code for Jam.py. A .xlsx (spreadsheet) importer is created entirely by ChatGPT. The video also demonstrates more about how to use SQL and other tools in command line sessions on a Linux server (with tmux), to integrate with Jam.py applications:

https://www.youtube.com/watch?v=DVoi0c1U1vg

Here's another quick video about using custom HTML, JS, CSS, jQuery, Python, etc. to extend Jam.py. A startup page and custom HTML templates are demonstrated and explored:

https://www.youtube.com/watch?v=oqv-0EuTUfw

This is a bit of a deeper dive into how to include custom front-end layouts (including plain old web page content, with a demo of how to quickly incorporate the open source Grapes visual builder), as well as how to populate custom HTML layouts with data from the database. The back end functions in the example use jam.py database features, and also demonstrate how to integrate SQLAlchemy database interactions. The goal is to explain how to extend the productive no-code capabilities of Jam.py with any sort of imaginable custom front-end layouts and back-end tooling:

https://www.youtube.com/watch?v=HdG1WnNz1iI

This video includes some more simple custom UI/database interaction examples (a coin flip simulator, date difference calculator, cash register system, and custom UI form-to-database example). There’s also a brief demonstration of how to use browser DevTools (the ‘F12’ console) to interact with Jam.py:

https://www.youtube.com/watch?v=nWsnispYiDE

This video demonstrates how to generate custom repeating panel (‘card’) layouts and how to integrate REST APIs calls on the back-end, with both the no-code Jam.py grids and Custom HTML UIs:

https://www.youtube.com/watch?v=kUjDKqPOBYE

This tutorial connects the Gemini API (Google's LLM) with Python on the back end of a Jam.py application, and integrates with a simple Jam.py front-end UI. The example application enables a user to upload PDF files, sends the file to the Gemini API, and prompts the model to return a summary of the PDF contents. The model is also prompted to provide diagnoses for any Z-ray images that are uploaded. It's dead simple to do with Jam.py, and this is a use case which is currently on everyone's mind, and actually practical in a wide variety of domains:

https://www.youtube.com/watch?v=4tXUqtnMMlk

This video demonstrates how to use Markdown to create web page layouts, and demonstrates how to integrate a Javascript audio recorder, which saves recordings to database rows, and enables selection and playback of saved audio files, all with Jam.py database tools.

https://www.youtube.com/watch?v=EnuJHqdVDTI

This video demonstrates how to use Grok AI to build a complete application with Jam.py:

https://www.youtube.com/watch?v=UjEsyKyeWl4

The full Grok conversation shown in that video is available at either of these links:

https://grok.com/share/bGVnYWN5_11f8ccd3-0c9a-4a0e-aa0d-c864c95efeaa

https://com-pute.com/nick/Jam.py%20in-context%20learning,%20poll%20example%20_%20Shared%20Grok%20Conversation.mhtml

The in-context training doc for LLM AIs, demonstrated in that video is available at either of these links:

https://com-pute.com/nick/jampy_llm_introduction.txt

https://drive.google.com/file/d/1dorsKVVUBka1ZgpC8lvoIuqVur160b0g/view

4. DOWNLOADABLE DEMO CODE

Here are the exported project zip files for all the examples above, which you can download and import directly into a Jam.py installation on your server. Each zip file contains all the code, schema, and everything required to run each of the complete application examples demonstrated in the Youtube videos, on your own server:

https://com-pute.com/nick/gemini_1.0.0_5.4.136_2025-02-12_14-15-48.zip

https://com-pute.com/nick/jam_form_with_api_and_repeating_panel_examples--1.0.0_5.4.136_2025-02-10_05-58-12.zip

https://com-pute.com/nick/jam_forum_1.0.0_5.4.136_2025-02-05_16-17-27.zip

https://com-pute.com/nick/jampy_html_landing_1.0.0_5.4.136_2025-01-30_17-56-52__with_textarea_which_displays_column_values_from_task_grid.zip

https://com-pute.com/nick/jampy_my_demo_1.0.0_5.4.136_2025-01-27_05-27-53.zip

https://com-pute.com/nick/xlsx_importer--jam_project_1.0.0_5.4.136_2025-01-19_20-57-10.zip

https://com-pute.com/nick/files_1.0.0_5.4.136_2025-02-02_17-30-00.zip

https://com-pute.com/nick/jampy_grok_poll_1.0.0_5.4.136_2025-02-21_11-20-25.zip

5. A TUTORIAL TO TEACH GENERATIVE AI TO USE JAM.PY

The following text is a human-readable tutorial, which can also be pasted into any AI chatbot (LLMs such as ChatGPT, Claude, Gemini, Deepseek, etc.) to provide enough in-context information to make the AI understand how to use the framework to generate working code. This prompt is available as plain text at the link below, so you can more easily copy it directly into your AI chat conversations:

https://com-pute.com/nick/jampy_llm_introduction.txt

https://drive.google.com/file/d/1dorsKVVUBka1ZgpC8lvoIuqVur160b0g/view

5.1 A copy-paste prompt to teach LLMs jam.py in-context

The following text explains how to use the Jam.py framework at https://jam-py.com

User interface pages in the Jam.py framework are called 'tasks'. Tasks are added to a Jam.py project with Jam.py's visual browser based IDE.

Each task has its own associated database table schema, which is built using the Jam.py IDE. Properties of the task UI and its database tables are referred to with dot notation (i.e., task.some_Object, task.some_Method, etc.). An editable front-end client Javascript code module is associated with each task. Similarly, a back-end server Python module is associated with each task. Both the back-end and front-end code of any task can access the database tables for associated task, or for any other task in the hierarchical task tree. Javascript functions can call Python server functions directly, and can use built-in jQuery event handlers, as well as built-in Bootstrap styling. In this way, the Jam.py framework eliminates any need for hand written AJAX code. The ORM included in Jam.py also eliminates the need for any hand written SQLAlchemy or 3rd party database tools.

Jam.py has a built-in no-code schema editor, and it automatically generates full-featured UI tables (data grids) and forms which are pre-wired to perform CRUD interactions with the database tables. The term 'item' is used to refer to the database rows displayed in auto-generated front-end UIs.

In order to build a custom layout and UI interface to the database (beyond the automatically generated grids and forms), HTML code can be added to a template section within the project index.html file. Templates are divs which can contain any valid HTML. The default index.html file can contain any CSS or other information in the head tags:

<!DOCTYPE html>


<html lang="en">
    <head>
        <meta charset="utf-8">
        <title></title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="icon" href="/jam/img/j.png" type="image/png">


        <link href="jam/css/bootstrap-cerulean.css" rel="stylesheet">            <!--do not modify-->
        <link href="jam/css/bootstrap-responsive.css" rel="stylesheet">
        <link href="jam/css/bootstrap-modal.css" rel="stylesheet">
        <link href="jam/css/datepicker.css" rel="stylesheet">
        <link href="jam/css/jam.css" rel="stylesheet">                  <!--do not modify-->
        <link href="css/project.css" rel="stylesheet">
    </head>

The index.html file also includes some defaut layout and templates used by jam.py:

<body>
    <div id="container" class="container" style="display: none">
        <div class="row-fluid">
            <div class="span6">
                <p id="project-title" class="muted"></p>
            </div>
            <div id="logging-info" class="span6">
                <span id="user-info"></span>
                <a id="log-out" href="#" style="display: none">Log out</a>
            </div>
        </div>
        <div id="taskmenu" class="navbar">
            <div class="navbar-inner">
                <ul id="menu" class="nav">
                </ul>
            </div>
        </div>
        <div id="content">
        </div>
    </div>


    <div class="templates" style="display: none">
        <div class="default-top-view">
            <div class="form-header">
                <button id="new-btn" class="btn expanded-btn" type="button">
                    <i class="icon-plus"></i> New<small class="muted">&nbsp;[Ctrl+Ins]</small>
                </button>
                <button id="edit-btn" class="btn expanded-btn" type="button">
                    <i class="icon-edit"></i> Edit
                </button>
                <div id="report-btn" class="btn-group dropup">
                    <a class="btn expanded-btn dropdown-toggle" data-toggle="dropdown" href="#">
                        <i class="icon-print"></i> Reports
                        <span class="caret"></span>
                    </a>
                    <ul class="dropdown-menu bottom-up">
                    </ul>
                </div>
                <button id="delete-btn" class="btn expanded-btn pull-right" type="button">
                    <i class="icon-trash"></i> Delete<small class="muted">&nbsp;[Ctrl+Del]</small>
                </button>
            </div>
            <div class="form-body">
                <div class="view-table"></div>
                <div class="view-detail"></div>
            </div>
        </div>


        <div class="default-view">
            <div class="form-body">
                <div class="view-table"></div>
                <div class="view-detail"></div>
            </div>
            <div class="form-footer">
                <button id="delete-btn" class="btn expanded-btn pull-left" type="button">
                    <i class="icon-trash"></i> Delete<small class="muted">&nbsp;[Ctrl+Del]</small>
                </button>
                <div id="report-btn" class="btn-group dropup">
                    <a class="btn expanded-btn dropdown-toggle" data-toggle="dropdown" href="#">
                        <i class="icon-print"></i>  Reports
                        <span class="caret"></span>
                    </a>
                    <ul class="dropdown-menu bottom-up">
                    </ul>
                </div>
                <button id="edit-btn" class="btn expanded-btn" type="button">
                    <i class="icon-edit"></i> Edit
                </button>
                <button id="new-btn" class="btn expanded-btn" type="button">
                    <i class="icon-plus"></i> New<small class="muted">&nbsp;[Ctrl+Ins]</small>
                </button>
            </div>
        </div>


        <div class="default-top-edit">
            <div class="form-header">
                <button type="button" id="ok-btn" class="btn btn-ary expanded-btn">
                    <i class="icon-ok"></i> OK<small class="muted">&nbsp;[Ctrl+Enter]</small>
                </button>
                <button type="button" id="cancel-btn" class="btn expanded-btn">
                    <i class="icon-remove"></i> Cancel
                </button>
            </div>
            <div class="form-body">
                <div class="edit-body"></div>
                <div class="edit-detail"></div>
            </div>
        </div>


        <div class="default-edit">
            <div class="form-body">
                <div class="edit-body"></div>
                <div class="edit-detail"></div>
            </div>
            <div class="form-footer">
                <button type="button" id="ok-btn" class="btn btn-ary expanded-btn">
                    <i class="icon-ok"></i> OK<small class="muted">&nbsp;[Ctrl+Enter]</small>
                </button>
                <button type="button" id="cancel-btn" class="btn expanded-btn">
                    <i class="icon-remove"></i> Cancel
                </button>
            </div>
        </div>


        <div class="default-param">
            <div class="form-body">
                <div class="edit-body">
                </div>
            </div>
            <div class="form-footer">
                <select id='extension' class="pull-left" style="width: 60px">
                    <option>pdf</option>
                    <option>ods</option>
                    <option>xls</option>
                    <option>html</option>
                </select>
                <button type="button" id="ok-btn" class="btn expanded-btn">
                    <i class="icon-print"></i> Print
                </button>
                <button type="button" id="cancel-btn" class="btn expanded-btn">
                    <i class="icon-remove"></i> Close
                </button>
            </div>
        </div>


        <div class="default-filter">
            <div class="form-body">
                <div class="edit-body">
                </div>
            </div>
            <div class="form-footer">
                <button type="button" id="ok-btn" class="btn expanded-btn">
                    <i class="icon-filter"></i> Apply
                </button>
                <button type="button" id="cancel-btn" class="btn expanded-btn">
                    <i class="icon-remove"></i> Close
                </button>
            </div>
        </div>


        <form id="login-form" target="dummy" class="form-horizontal" data-caption="Log in" style="margin: 10px 0 0; padding: 0px">
            <div class="control-group">
                <label class="control-label" for="input-login">Login</label>
                <div class="controls">
                    <input type="text" id="input-login" name="login" tabindex="1" placeholder="login">
                </div>
            </div>
            <div class="control-group">
                <label class="control-label" for="input-password">Password</label>
                <div class="controls">
                    <input type="password" id="input-password" name="password" tabindex="2" placeholder="password" autocomplete="on">
                </div>
            </div>
            <div class="form-footer">
                <input type="submit" class="btn expanded-btn pull-right" id="login-btn" value="OK" tabindex="3">
            </div>
        </form>


    </div>
    <iframe src="dummy.html" name="dummy" style="display: none"></iframe>

Javascript libraries can be imported, and you can include any custom JS in the script section of the index.html file:

<iframe src="dummy.html" name="dummy" style="display: none"></iframe>


<script src="jam/js/jquery.js"></script>
<script src="jam/js/bootstrap.js"></script>
<script src="jam/js/bootstrap-modal.js"></script>
<script src="jam/js/bootstrap-modalmanager.js"></script>
<script src="jam/js/bootstrap-datepicker.js"></script>
<script src="jam/js/jquery.maskedinput.js"></script>
<script src="jam/js/jam.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!--<script src="static/js/marked.min.js"></script>-->
<!--<script src="static/js/Chart.min.js"></script>-->


<script>
$(document).ready(function(){
    task.load();
  });
</script>


</body>
</html>

Custom CSS code is typically added to the project.css file.

Here are some examples of custom Jam.py templates which have been added to an index.html in various projects:

<div class="cointoss">
    <h1 id="coinheader">Coin toss</h1>
    <div class="coinholderdiv">
        <img id="coinimage" height=159 width=159>
    </div>
    <br>
    <form id="coinform">
        <button type="submit" id="coinbutton">Flip</button>
    </form>
</div>




<div class="api-repeating-panel"></div>




<div class="webcam-repeating-panel"></div>




<div class="markdown-container"></div>




<div class="audio-recorder">
    <br>
    <button id="recordButton">Start New Recording</button><br><br>
    <audio id="audioPlayer" controls></audio><br><br>
    <button id="playButton">Play Selected Recording</button>
</div>




<div class="custom-form">
    <h1 id="ifg9">A Form</h1>
    <p id="il8i">This form was created with the Grapes.js visual layout editor.  The HTML code generated by Grapes.js was copied into the Jam.py index.html file (as divs in the template section), and the CSS code was copied directly into the Jam.py project.css file.<br/><br/>In Jam.py, a new task page is then created.  On that task page, the saved HTML code is imported from the template section of the index.html file.  This import is performed in the client JS code of the task page, within the on_view_form_created handler.  In the client JS code, a click event handler is also written for the generated HTML button.  In that JS handler function, the data collected from each form field is sent to a Python function on the Jam.py back-end.  In the back-end function, Jam.py database functions are used to save the form data values to a database table, which has been created using the no-code schema builder in Jam.py.<br/><br/>Another task page in the Jam.py project is also created, on which an automatically generated Jam.py grid displays all the rows of data which have ever been entered into the custom HTML form.  The automatically generated set of Jam.py grid and form UI components are capable of enabling all the same functionality of the custom Grapes.js form, without having to write any code at all.  The point of this example is just to show how simple it is to wire custom UI interfaces to any of the database schema tables and UI grids/forms which can be created using Jam.py's no-code tools.</p>
    <form id="ifv3g">
        <div id="isq5a"><label for="multi-text">Text</label><input type="text" id="multi-text" name="text" placeholder="Enter text"/></div>
        <div id="iit25"><label for="multi-email">Email</label><input type="email" id="multi-email" name="email" placeholder="Enter email"/></div>
        <div id="ikr4s"><label for="multi-password">Password</label><input type="password" id="multi-password" name="password" placeholder="Enter password"/></div>
        <div id="ijt1a">
            <label>Gender</label>
            <div><label><input type="radio" name="gender" value="male"/> Male</label><label><input type="radio" name="gender" value="female"/> Female</label></div>
        </div>
        <div id="i0106">
            <label for="multi-select">Select Option</label>
            <select id="multi-select" name="option">
                <option value="">Select an option</option>
                <option value="1">Option 1</option>
                <option value="2">Option 2</option>
            </select>
        </div>
        <div id="iqofq"><label for="multi-date">Date</label><input type="date" id="multi-date" name="date"/></div>
        <div id="ihw0f"><label for="multi-number">Number</label><input type="number" id="multi-number" name="number" placeholder="Enter a number"/></div>
        <div><button type="submit" id="i5a3g">Submit</button></div>
    </form>
</div>

In order to display a template layout on a task page, and to apply Javascript code to tags in the layout, we find a specific template class, and then typically clone and display that template, either in place of the default generated Jam.py 'view_form' layout:

function on_view_form_created(item) {
    const template = task.templates.find(".markdown-container").clone();
    item.view_form.empty().append(template);
}

or along with the generated Jam.py layout:

function on_view_form_created(item) {
    const template = task.templates.find(".markdown-container").clone();
    item.view_form.append(template);
}

We use jQuery events to handle events within the default UI grid, form, and page layout. These handlers are built-in:

function on_after_append(item) {}
function on_after_apply(item) {}
function on_after_cancel(item) {}
function on_after_delete(item) {}
function on_after_edit(item) {}
function on_after_open(item) {}
function on_after_post(item) {}
function on_after_scroll(item) {}
function on_before_append(item) {}
function on_before_apply(item, params) {}
function on_before_cancel(item) {}
function on_before_delete(item) {}
function on_before_edit(item) {}
function on_before_field_changed(field) {}
function on_before_open(item, params) {}
function on_before_post(item) {}
function on_before_scroll(item) {}
function on_detail_changed(item, detail) {}
function on_edit_form_close_query(item) {}
function on_edit_form_closed(item) {}
function on_edit_form_created(item) {}
function on_edit_form_keydown(item, event) {}
function on_edit_form_keyup(item, event) {}
function on_edit_form_shown(item) {}
function on_field_changed(field, lookup_item) {}
function on_field_get_html(field) {}
function on_field_get_summary(field, value) {}
function on_field_get_text(field) {}
function on_field_select_value(field, lookup_item) {}
function on_field_validate(field) {}
function on_filter_changed(filter) {}
function on_filter_form_close_query(item) {}
function on_filter_form_closed(item) {}
function on_filter_form_created(item) {}
function on_filter_form_shown(item) {}
function on_filter_record(item) {}
function on_filter_select_value(field, lookup_item) {}
function on_filters_applied(item) {}
function on_filters_apply(item) {}
function on_view_form_close_query(item) {}
function on_view_form_closed(item) {}
function on_view_form_created(item) {}
function on_view_form_keydown(item, event) {}
function on_view_form_keyup(item, event) {}
function on_view_form_shown(item) {}

Within Javascript functions, we can directly call back-end Python functions, passing any argument values from the front-end JS. Then we can apply any imported JS library functions to the data returned from the back-end. For example, this code gets markdown code from a back-end Python function called get_markdown(), and then parses & displays the rendered markdown code within the cloned custom ".markdown-container" template layout:

function on_view_form_created(item) {
    const template = task.templates.find(".markdown-container").clone();
    item.view_form.empty().append(template);
    // Call the server function to get Markdown text
    item.server("get_markdown", function (markdownText, error) {
        if (error) {
            console.error("Error fetching Markdown:", error);
            return;
        }
        // Convert Markdown to HTML using the imported marked JS library
        let htmlContent = marked.parse(markdownText);
        // Insert HTML into the UI
        item.view_form.find(".markdown-container").html(htmlContent);
    });
}

Here is the Python function which returns markdown code to the JS function above:

def get_markdown(item):  #item parameter is required from Jam.py 
    return """
# Welcome to the App
This is a **bold** statement.


- Item 1
- Item 2
- Item 3


[Click Here](https://example.com) for more info
"""

Jam.py enables database operations to be accessed directly in front-end JavaScript code. The example below creates a new row in the table associated with the 'form_tables' task, and saves the values which the user has entered into the 'custom_form' HTML template:

function on_view_form_created(item) {
    const template = task.templates.find(".custom-form").clone();
    item.view_form.empty().append(template);
    $("#ifv3g").submit(function (event) {
        event.preventDefault(); // Prevent default page reload
        let c = task.form_tables.copy();
        c.open();
        c.append();
        c.text.value=$("#multi-text").val();
        c.email.value=$("#multi-email").val();
        c.password.value=$("#multi-password").val();
        c.gender.value=$("input[name='gender']:checked").val() || null;
        c.options.value=$("#multi-select").val();
        c.date.value = $("#multi-date").val() ? new Date($("#multi-date").val() + "T00:00:00") : null;
        c.number.value=$("#multi-number").val();
        c.created_at.value=new Date();
        c.post();
        c.apply();
    });
}

Custom code can be written to handle events of any sort, in the UI forms and grids which are automatically generated by Jam.py (and/or in any custom template code):

function on_field_changed(field, lookup_item) {
    field.owner.difference.value = (
        field.owner.end_date.value - field.owner.start_date.value
    ) / (1000 * 60 * 60 * 24);
}


function on_field_validate(field) {
    if (field.owner.end_date.value < field.owner.start_date.value) {
        return "End date must be later than start date";
    }
}

Images and files which are uploaded by automatically generated Jam.py forms, are stored in the ./static/files/ sub-folder of the project directory. They can be accessed and used in custom layouts:

function on_view_form_created(item) {
    const template = task.templates.find(".cointoss").clone();
    item.view_form.empty().append(template);
$("#coinform").submit(function (event) {
    event.preventDefault();
    const images = [
        "./static/files/tails_2025-02-08_17-43-48_211693.jpg",
        "./static/files/heads_2025-02-08_17-43-34_191100.jpg"
    ];
    const randomImage = images[Math.floor(Math.random() * images.length)];
    $("#coinimage").attr("src", randomImage);
    console.log("Flipped to:  " + randomImage);
});
}

In the example below, notice that the 'item' parameter (which represents database values in auto-generated UI grid values) is passed to the Python server function from the Jam.py front-end JS function. This is always required, whether or not those values are needed by the back end function. In this example, values returned from the back-end REST API call, is used on the front end:

import requests
def guess_age(item, person):   # the 'item' parameter is required
  r = requests.get(f'https://api.agify.io/?name={person}').json()
  return r["age"]




function on_field_changed(field, lookup_item) {
    let name = field.value; // Get the entered name
    # The 'item' parameter is NOT explicitly passed by the front end:
    field.owner.server("guess_age", name, function (age, error) {
        if (age) {
            field.owner.guessed_age.value = age;
        }
    });
}

Here's another example of the response to a back-end API call, being returned and used in a front end function:

function on_field_changed(field, lookup_item) {
    let zipCode = field.value; // Get the entered zip code


    field.owner.server("get_weather", zipCode, function (weatherData, error) {
        if (error) {
            console.error("Error fetching weather data:", error);
            return;
        }


        if (weatherData) {
            // Update the fields with fetched temperature and wind speed
            field.owner.temperature.value = weatherData.temp;
            field.owner.wind_speed.value = weatherData.wind_speed;
        } else {
            console.warn("No weather data received.");
        }
    });
}




import os
import requests


def get_weather(item, zipc):
    """
    Fetch weather data from OpenWeather API using a zip code.
    :param item: Required by Jam.py (not used)
    :param zipc: Zip code (string or integer)
    :return: Dictionary containing temperature and wind speed or None on error
    """
    api_key = os.getenv("OPENWEATHER_API_KEY")  # Read from environment


    if not api_key:
        print("❌ ERROR: Missing API Key! Set OPENWEATHER_API_KEY in the environment.")
        return None


    url = f"https://api.openweathermap.org/data/2.5/weather?zip={zipc}&appid={api_key}&units=imperial"


    try:
        response = requests.get(url)
        response.raise_for_status()
        data = response.json()
return {
    "temp": data["main"]["temp"],
    "wind_speed": data["wind"]["speed"]
}
except requests.exceptions.RequestException as e:
print(f"Error fetching weather data: {e}")
return None

A common requirement in web application development is to build 'repeating-panel' or 'card' layouts from data returned by server functions (imagine repetitive Amazon search result layouts, blog post layouts, etc.). Here's an example of how to build and display such layouts:

import requests


def get_json_data(item):
    url = "https://jsonplaceholder.typicode.com/users"
    try:
        response = requests.get(url)
        response.raise_for_status()  # Raise an error for bad status codes
        users = response.json()      # Parse the JSON response into a Python object (list/dict)
        print(users)
        return users
    except requests.exceptions.RequestException as e:
        print(f"An error occurred: {e}")
        return None




function on_view_form_created(item) {
    // Clone the template that contains the repeating panel container
    const template = task.templates.find(".api-repeating-panel").clone();
    item.view_form.empty().append(template);
function renderLayout() {
    item.server("get_json_data", function (rows, error) {
        if (error) {
            console.error("Error fetching topics:", error);
            return;
        }
// Find the repeating panel container within the view form
let repeatingPanel = item.view_form.find(".api-repeating-panel");
repeatingPanel.empty();
// Loop through each user (row) and create a detailed card.
rows.forEach(rowItem => {
    // Create the main card container with additional custom styling
    let card = $("<div>")
        .addClass("card custom-card mb-4")
        .data("rowItem-id", rowItem.id);
// Card header with a gradient background displaying the name and username
let cardHeader = $("<div>")
    .addClass("card-header")
    .html(`<h4>${rowItem.name}</h4>
          <small>@${rowItem.username}</small>`);
// Card body for the main details
let cardBody = $("<div>").addClass("card-body");
// Basic user details: email, phone, and website
cardBody.append(`
    <p class="card-text"><strong>Email:</strong> ${rowItem.email}</p>
    <p class="card-text"><strong>Phone:</strong> ${rowItem.phone}</p>
    <p class="card-text">
        <strong>Website:</strong> 
        <a href="http://${rowItem.website}" target="_blank">${rowItem.website}</a>
    </p>
`);
// Address details from the nested address object
const addr = rowItem.address;
cardBody.append(`
    <div class="address">
        <p class="card-text"><strong>Address:</strong> ${addr.street}, ${addr.suite}</p>
        <p class="card-text">${addr.city}, ${addr.zipcode}</p>
        <p class="card-text"><small>Geo: lat ${addr.geo.lat}, lng ${addr.geo.lng}</small></p>
    </div>
`);
// Company details from the nested company object
const comp = rowItem.company;
cardBody.append(`
    <div class="company">
        <p class="card-text"><strong>Company:</strong> ${comp.name}</p>
        <p class="card-text"><em>"${comp.catchPhrase}"</em></p>
        <p class="card-text">${comp.bs}</p>
    </div>
`);
// Assemble the card
card.append(cardHeader);
card.append(cardBody);
// Append the card to the repeating panel container
repeatingPanel.append(card);
});
});
}
renderLayout();
}

Here's an another repeating card layout example, displaying a list of images stored in a JS list:

function on_view_form_created(item) {
    // Clone the template that contains the webcam-repeating-panel
    const template = task.templates.find(".webcam-repeating-panel").clone();
    item.view_form.empty().append(template);
function renderLayout() {
    const baseUrl = "https://cwwp2.dot.ca.gov/data/d2/cctv/image/";
    const imagePaths = [
        "pitriverbridge/pitriverbridge.jpg",
        "mthebron/mthebron.jpg",
        "eurekaway/eurekaway.jpg",
        "sr70us395/sr70us395.jpg",
        "bogard/bogard.jpg",
        "eastriverside/eastriverside.jpg",
    ];


    let repeatingPanel = item.view_form.find(".webcam-repeating-panel");
    repeatingPanel.empty();
imagePaths.forEach(camurl => {
    let fullImageUrl = baseUrl + camurl;


    let card = $("<div>").addClass("card webcam-card mb-4");


    let imgElement = $("<img>")
        .attr("src", fullImageUrl)
        .attr("alt", "Traffic Camera")
        .addClass("card-img-top");


    let cardBody = $("<div>").addClass("card-body");
    cardBody.append(`<p class="card-text">${camurl.split("/")[0]}</p>`);


    card.append(imgElement);
    card.append(cardBody);
    repeatingPanel.append(card);
});
}
renderLayout();
}

The following example demonstrates how to record audio using the JS 'getUserMedia' API, then convert the audio data to base64 format, save the base64 data to the database using Python server functions, and finally retrieve the data via Python server code, and play the retrieved file with JS code:

function on_view_form_created(item) {
    // Clone the audio recorder template and append it to the view
    const template = task.templates.find(".audio-recorder").clone();
    item.view_form.append(template);


    let mediaRecorder;
    let audioChunks = [];
    let isRecording = false;
    let audioStream; // variable to store the stream


    // Select elements within the view form
    const recordButton = item.view_form.find("#recordButton");
    const audioPlayer = item.view_form.find("#audioPlayer");
    const playButton = item.view_form.find("#playButton");


    // Attach event listeners correctly using .on() inside the function
    recordButton.on("click", function () {
        toggleRecording();
    });


    playButton.on("click", function () {
        loadAndPlayAudio();
    });


    function startRecording() {
        if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
            console.error("❌ getUserMedia is not supported in this browser!");
            alert("Audio recording is not supported in your browser. Please update or use Chrome/Edge/Firefox.");
            return;
        }


        navigator.mediaDevices.getUserMedia({ audio: true })
            .then(stream => {
                audioStream = stream; // save the stream
                mediaRecorder = new MediaRecorder(stream);
                mediaRecorder.start();
                isRecording = true;
                audioChunks = [];


                mediaRecorder.ondataavailable = event => {
                    audioChunks.push(event.data);
                };


                mediaRecorder.onstop = () => {
                    const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
                    convertToBase64(audioBlob, function (base64Audio) {
                        saveAudioToDatabase(base64Audio);
                    });
                };


                recordButton.text("Stop Recording");
            })
            .catch(err => {
                console.error("❌ Microphone access denied!", err);
                alert("❌ Microphone access was denied. Please enable it in browser settings.");
            });
    }


    function stopRecording() {
        if (mediaRecorder && isRecording) {
            mediaRecorder.stop();
            // Stop all tracks of the stream to release the microphone
            if (audioStream) {
                audioStream.getTracks().forEach(track => track.stop());
            }
            isRecording = false;
            recordButton.text("Start Recording");
        }
    }


    function toggleRecording() {
        isRecording ? stopRecording() : startRecording();
    }


    function convertToBase64(blob, callback) {
        let reader = new FileReader();
        reader.readAsDataURL(blob);
        reader.onloadend = function () {
            callback(reader.result.split(",")[1]); // Extract only Base64
        };
    }


    function saveAudioToDatabase(base64Audio) {
        item.server("save_audio", base64Audio, function (response, error) {
            if (error) {
                console.error("❌ Error saving audio:", error);
                alert("❌ Failed to save the audio.");
            } else {
                console.log("✅ Audio saved successfully!", response);
                alert("✅ Audio saved successfully!");
            }
        });
    }


    function loadAndPlayAudio() {
        // Use rec_no from the current dataset to get the selected record number.
        // Adjust this according to your dataset structure—if your audio records dataset is a sub-table,
        // replace "item.rec_no" with the correct reference, e.g. "item.audio_table.rec_no".
        let currentRecNo = item.rec_no;
if (currentRecNo === undefined || currentRecNo === null) {
    alert("No record is selected!");
    return;
}
// Pass the current record number to the server function "get_audio"
item.server("get_audio", currentRecNo, function(base64Audio, error) {
    if (error) {
        console.error("❌ Error fetching audio:", error);
        alert("❌ Failed to fetch audio.");
        return;
    }
if (base64Audio) {
    audioPlayer.attr("src", "data:audio/webm;base64," + base64Audio);
    audioPlayer[0].play();
} else {
    alert("No audio file found for the selected record.");
}
});
}
}




import datetime
import binascii


def save_audio(item, base64_audio):
"""
Saves recorded audio to the database.
:param item: Required by Jam.py
:param base64_audio: The Base64-encoded audio data
:return: Confirmation message
"""
try:
    item.open()
    item.append()
    item.audio_data.value = base64_audio
    item.timestamp.value = datetime.datetime.now()
    item.post()
    item.apply()
    return "Audio saved successfully"
except Exception as e:
    print(f"Error saving audio: {e}")
    return "Error saving audio"




def get_audio(item, record_id):
"""
Fetches the recorded audio from the audio_records table for the given record_id.
:param item: Required by Jam.py.
:param record_id: The record number (rec_no) of the audio record to retrieve.
:return: Base64 audio data from the specified record or None.
"""
try:
    # Open the dataset
    item.open()
    # Set the current record using rec_no.  Note that rec_no is 0-based.
    item.rec_no = record_id  
    the_sound_file = item.audio_data.value
    item.close()
    return the_sound_file
except Exception as e:
    print(f"Error fetching audio for record {record_id}: {e}")
    return None

Here are some more examples of how to interact with the built-in UI components generated by Jam.py, how to display custom HTML within a default layout, and how to perform some other various client interactions:

function on_view_form_created(item) {
    // Ensure the correct template is used
    const template = task.templates.find(".journal-view").clone();
    item.view_form.empty().append(template);


    // Add dynamic behavior or adjustments
    item.view_form.find(".custom-header").addClass("header-styled");


    // Customize table options and initialize the grid
    item.table_options.height -= 100; // Adjust grid height
    item.create_table(item.view_form.find(".view-table"));


    // Initialize buttons
    const newButton = item.view_form.find("#new-btn");
    const editButton = item.view_form.find("#edit-btn");
    const deleteButton = item.view_form.find("#delete-btn");


    // Assign event handlers to buttons
    if (item.can_create()) {
        newButton.on("click.task", function () {
            item.insert_record();
        });
    } else {
        newButton.prop("disabled", true);
    }


    if (item.can_edit()) {
        editButton.on("click.task", function () {
            item.edit_record();
        });
    } else {
        editButton.prop("disabled", true);
    }


    if (item.can_delete()) {
        deleteButton.on("click.task", function () {
            item.delete_record();
        });
    } else {
        deleteButton.prop("disabled", true);
    }


    // Log success message
    console.log("Grid and buttons initialized successfully for persons task.");
}

The JS code above uses the following custom template:

<div class="journal-view">
        <div class="custom-header">
                <h2>This Is More Custom HTML</h2>
                <p>Click the catalogue links above to see app examples embedded in iframes.</p>
                <p>This is Deb, she's the best:</p><br>
        <img src="https://antonaccio.net/nick/debtiny.jpg">
        </div>
        <div class="form-body">
                <div class="view-table"></div>
                <div class="view-detail"></div>
        </div>
        <div class="form-footer">
                <button id="new-btn" class="btn expanded-btn" type="button">
                        <i class="icon-plus"></i> New
                </button>
                <button id="edit-btn" class="btn expanded-btn" type="button">
                        <i class="icon-edit"></i> Edit
                </button>
                <button id="delete-btn" class="btn expanded-btn" type="button">
                        <i class="icon-trash"></i> Delete
                </button>
        </div>
</div>

The example below uses JS to place a text area in the UI layout, and calls a server function, to display all the values from each database row, in the column_1 column:

function on_view_form_created(item) {
    console.log("Fetching full dataset from server...");


    item.server("get_all_column_1_values", function(values, error) {
        if (error) {
            console.error("Error fetching data from server:", error);
            return;
        }


        console.log("Received full dataset from server:", values);


        let textArea = item.view_form.find("#column_1_values");
        textArea.val(values.length ? values.join("\n") : "No valid column_1 data found.");


        console.log("Text area updated with column_1 values.");
    });
}




def get_all_column_1_values(item):
    """Fetch all values from column_1 in the database table and return them."""
    item.open()  # Ensure the dataset is fully opened
    values = [row.column_1.value for row in item if row.column_1.value]  # Collect non-empty values
    return values  # Send back the list of values

Here's the client JS code of a task which simply displays an iframe containing another application (written using the SQLPage framework, which connects to the same database as the Jam.py application):

function on_view_form_created(item) {
    const template = task.templates.find(".sqlpage-iframe-view").clone();
    item.view_form.empty().append(template);
}

Here's the HTML template used by that JS:

<div class="sqlpage-iframe-view">
        <div class="iframe-container" style="text-align: center; margin-top: 20px;">
                <iframe 
                        src="http://server.py-thon.com:8008/contacts_simple.sql" 
                        style="width: 100%; height: 147vh; border: none;"
                        title="SQLPage Application">
                </iframe>
        </div>
</div>

And just for reference (not needed for any Jam.py coding), here is the SQLPage code which can be used to create such an app, which connects to the database create by Jam.py:

-- Insert new record only if all fields are provided and no edit is in progress
INSERT INTO JAM_FORUM_POSTS (CONTENT, CREATED_AT, DELETED, AUTHOR, MASTER_REC_ID, MASTER_ID)
SELECT :Content, :Created_At, :Deleted, :Author, :Master_Rec_ID, :Master_ID
WHERE :Content IS NOT NULL AND $e IS NULL;


-- Update the record when editing
UPDATE JAM_FORUM_POSTS
SET CONTENT = :Content, CREATED_AT = :Created_At, DELETED = :Deleted, AUTHOR = :Author, MASTER_REC_ID = :Master_Rec_ID, MASTER_ID = :Master_ID 
WHERE id = $e AND :Content IS NOT NULL;


-- Delete the record
DELETE FROM JAM_FORUM_POSTS WHERE id = $d;


-- Conditionally show the form for editing or adding a new entry
SELECT 'form' AS component;


-- Populate form fields for both adding and editing
SELECT (SELECT CONTENT FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Content' AS name;
SELECT (SELECT CREATED_AT FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Created_At' AS name;
SELECT (SELECT DELETED FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Deleted' AS name;
SELECT (SELECT MASTER_REC_ID FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Master_Rec_ID' AS name;
SELECT (SELECT MASTER_ID FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Master_ID' AS name;
SELECT (SELECT AUTHOR FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Author' AS name;




-- Add "Add New" button to set the $add parameter
SELECT 'button' AS component, 'center' AS justify;
SELECT '?add=1' AS link, 'Clear and Add New' AS title;


-- Display the table with actions
SELECT 'table' AS component, 
    'Edit' AS markdown, 
    'Remove' AS markdown, 
    TRUE AS sort,
    TRUE AS search;


SELECT  
    id AS ID, 
    CONTENT AS Content, 
    CREATED_AT AS Created_At,
    DELETED AS Deleted,
    AUTHOR AS Author,
    MASTER_REC_ID AS Master_Rec_ID,
    MASTER_ID AS Master_ID,
    '[Edit](?e=' || id || ')' AS Edit,          -- Dynamic link for edit
    '[🗑️](?d=' || id || ')' AS Remove           -- Dynamic link for delete
FROM JAM_FORUM_POSTS;

When using another framework to interact with database tables created by a Jam.py application, just be sure to set the path to the database created by your Jam.py app, in your database configuration file. For example, for the SQLPage app above:

{
   "database_url": "sqlite://../jampy_folder/sqlite_database_filename_created_by_jampy?mode=rwc"
}

This example loads an RShiny app in an iframe, along with displaying the UI grid which is automatically generated by Jam.py:

function on_view_form_created(item) {
    // Load the correct template
    const template = task.templates.find(".shiny-iframe-view").clone();
    item.view_form.empty().append(template);


    // Debug: Ensure view-table exists
    const tableContainer = item.view_form.find(".view-table");
    if (tableContainer.length === 0) {
        console.error("Error: view-table container not found.");
        return;
    }


    // Initialize the grid
    item.table_options.height = 300; // Adjust height as needed
    item.create_table(tableContainer);


    // Initialize buttons
    const newButton = item.view_form.find("#new-btn");
    const editButton = item.view_form.find("#edit-btn");
    const deleteButton = item.view_form.find("#delete-btn");


    if (item.can_create()) {
        newButton.on("click.task", function () {
            item.insert_record();
        });
    } else {
        newButton.prop("disabled", true);
    }


    if (item.can_edit()) {
        editButton.on("click.task", function () {
            item.edit_record();
        });
    } else {
        editButton.prop("disabled", true);
    }


    if (item.can_delete()) {
        deleteButton.on("click.task", function () {
            item.delete_record();
        });
    } else {
        deleteButton.prop("disabled", true);
    }


    console.log("Iframe, grid, and buttons loaded successfully for shiny_iframe task.");
}

Here's the HTML template used by the JS above:

<div class="shiny-iframe-view">
  <div class="form-body">
    <div class="view-table"></div>
  </div>
  <div class="form-footer">
    <button id="new-btn" class="btn expanded-btn" type="button">
      <i class="icon-plus"></i> New
    </button>
    <button id="edit-btn" class="btn expanded-btn" type="button">
      <i class="icon-edit"></i> Edit
    </button>
    <button id="delete-btn" class="btn expanded-btn" type="button">
      <i class="icon-trash"></i> Delete
    </button>
  </div>
  <div class="iframe-container">
    <iframe 
      src="http://appstagehost.com:3872" 
      style="width: 100%; height: 119vh; border: none;"
      title="Shiny Application">
    </iframe>
  </div>
</div>

Here's how a splash page with animated imagery can be added to the startup of a Jam.py app, within $(document).ready(function(){}):

<script>
    $(document).ready(function () {
        // Show the application when the "Enter Application" button is clicked
        $("#start-app-btn").click(function () {
            $("#landing-page").hide();  // Hide the landing page
            $("#container").show();    // Show the main application container
            task.load();               // Initialize the Jam.py application
        });
    });
const canvas = document.getElementById('asciiCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// ASCII art layout
const asciiArt = [
"  \\o/",
"  +--------+--------+",
"  !        !        !",
"  !        !        !",
"  !        !        !",
"  !        !        !",
"  !        !        !",
"  !        !        !",
"  !        !        !",
"  +--------+--------+",
"  !        !        !",
"  !        !        !",
"  !        !        !",
"  !        !        !",
"  !        !        !",
"  !        !        !",
"  !        !        !",
"  +--------+--------+"
];
// Convert ASCII to particles
const particles = [];
const fontSize = 10; // Font size for each ASCII character
const centerX = canvas.width / 2.38;
const centerY = canvas.height / 4;
asciiArt.forEach((line, rowIndex) => {
for (let colIndex = 0; colIndex < line.length; colIndex++) {
  const char = line[colIndex];
  if (char.trim()) {
    particles.push({
      x: centerX + (colIndex - line.length / 2) * fontSize,
      y: centerY + (rowIndex - asciiArt.length / 2) * fontSize,
      char,
      vx: Math.random() * 10 - 5,
      vy: Math.random() * 10 - 5,
      originalX: centerX + (colIndex - line.length / 2) * fontSize,
      originalY: centerY + (rowIndex - asciiArt.length / 2) * fontSize,
    });
  }
}
});
let explosionPhase = true; // Whether particles are exploding
let time = 0; // Animation time tracker
let pauseTime = 0; // Pause time tracker
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black';
// ctx.fillStyle = 'white';
ctx.font = `${fontSize}px monospace`;
particles.forEach((particle) => {
  if (explosionPhase) {
    particle.x += particle.vx;
    particle.y += particle.vy;
  } else {
    particle.x += (particle.originalX - particle.x) * 0.05;
    particle.y += (particle.originalY - particle.y) * 0.05;
  }
ctx.fillText(particle.char, particle.x, particle.y);
});
if (explosionPhase && time > 60) {
  explosionPhase = false;
} else if (!explosionPhase && time > 120) {
  // Pause for a few seconds before restarting the animation
  if (pauseTime < 180) {
    pauseTime++;
  } else {
    explosionPhase = true;
    time = 0;
    pauseTime = 0;
  }
} else {
  time++;
}
requestAnimationFrame(draw);
}
draw();
</script>

This example demonstrates how to build a custom front end to a forum application, which is built on a set of master-details tables created by the Jam.py visual builder:

function on_view_form_created(item) {
    console.log("on_view_form_created executed");
    let forumContainer = $("#forum-container"); 


    function renderTopics() {
        item.server("get_all_topics", function (topics, error) {
            if (error) return console.error("Error fetching topics:", error);
            let topicList = $("#topic-list");
            topicList.empty();
            topics.forEach(topic => {
                let topicElement = $("<li>")
                    .addClass("list-group-item forum-topic")
                    .text(topic.title)
                    .data("topic-id", topic.id)
                    .click(() => loadPosts(topic.id));  // Click loads posts but doesn't hide topics
                topicList.append(topicElement);
            });
        });
    }


    function loadPosts(topicId) {
        console.log("Loading posts for topic ID:", topicId);
        item.server("get_posts_by_topic", [topicId], function (posts, error) {
            if (error) return console.error("Error fetching posts:", error);
            console.log("Received posts:", posts);
            let forumContainer = $("#forum-container");
            if (forumContainer.length === 0) {
                console.warn("#forum-container NOT FOUND! Creating it now...");
                $("body").append('<div id="forum-container"></div>'); // Create container
                forumContainer = $("#forum-container"); // Re-select after adding
            }
            // Remove previous posts section before adding a new one
            $("#simple-posts").remove();
            // Add a new div to display posts
            forumContainer.append("<div id='simple-posts' style='border:1px solid blue; padding:10px; margin-left:8%; margin-right:8%'></div>");
            let postList = $("#simple-posts"); // Select newly created div
            postList.empty(); // Clear old posts
            postList.append("<h3>Posts</h3>"); // This will be the ONLY header
            // Add "New Post" button
            postList.append("<button id='create-post-btn' class='btn btn-primary'>New Post</button>");
            if (posts.length === 0) {
                postList.append("<div>No posts yet. Be the first to post!</div>");
            } else {
                posts.forEach(post => {
                    postList.append(`<div><b>${post.author}:</b> ${post.content} <br> <small>${post.created_at}</small></div>`);
                });
            }
            console.log("Posts successfully updated.");
            $("#create-post-btn").click(() => {
                let author = prompt("Enter your name:");
                let content = prompt("Enter post content:");
                if (!author || !content) return;
                console.log(`Attempting to create post for topic: ${topicId}`);
                item.server("create_post", [topicId, author, content], function(response, error) {
                    if (error) {
                        console.error("Error creating post:", error);
                        return;
                    }
                    if (response.error) {
                        console.error("Server Error:", response.error);
                    } else {
                        console.log("Post created successfully, refreshing posts...");
                        loadPosts(topicId);
                    }
                });
            });
        });
    }


    $("#create-topic-btn").click(() => {
        let title = prompt("Enter topic title:");
        if (!title) return;
        item.server("create_topic", [title], function (response, error) {
            if (error) return console.error("Error creating topic:", error);
            renderTopics();
        });
    });


    forumContainer.append($(".topics-view").clone());  // Ensures the topic list is always present
    renderTopics();  // Load topics immediately
}

The HTML template used by that example code is:

<div id="forum-container">
     Topics view 
    <div class="topics-view">
        <div class="form-body">
            <h2>Forum Topics</h2>
            <button id="create-topic-btn" class="btn btn-primary">New Topic</button>
            <ul id="topic-list" class="list-group"></ul>
        </div>
    </div>


     Posts view 
    <div class="posts-view" style="display: none;">
        <div class="form-body">
            <button id="back-to-topics" class="btn btn-secondary">Back</button>
            <h3>Posts</h3>
            <button id="create-post-btn" class="btn btn-primary">New Post</button>
            <div id="post-list" class="list-group"></div>
        </div>
    </div>
</div>

This back-end code demonstrates how to not only use the Jam.py ORM, but also how to integrate custom SQLAlchemy code, to interact with the database tables created by Jam.py:

from datetime import datetime
import sqlalchemy
from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey, MetaData, inspect
from sqlalchemy.orm import relationship, declarative_base, Session


Base = declarative_base()
DATABASE_URL = "sqlite:////root/jamforum3/jam_forum3"
engine = create_engine(DATABASE_URL, echo=True)


class Topic(Base):
    __tablename__ = "JAM_FORUM_TOPICS"
    id = Column(Integer, primary_key=True)
    title = Column(String, nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    posts = relationship("Post", back_populates="topic")


class Post(Base):
    __tablename__ = "JAM_FORUM_POSTS"
    id = Column(Integer, primary_key=True)
    deleted = Column(Integer, default=0)  # Default to 0 like Jam.py
    master_id = Column(Integer, ForeignKey("JAM_FORUM_TOPICS.id"), nullable=False)  # Matches MASTER_ID
    master_rec_id = Column(Integer, nullable=True)  # Mimic Jam.py's MASTER_REC_ID
    author = Column(String, nullable=False)
    content = Column(String, nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    topic = relationship("Topic", back_populates="posts")


def create_post(self, topic_id, author, content):  #(topic_id, author, content):
    try:
        print(f"Creating new post for topic {topic_id}...")
        with Session(engine) as session:
            # Fetch topic to determine MASTER_ID  # MASTER_REC_ID
            topic = session.query(Topic).filter(Topic.id == topic_id).first()
            if not topic:
                print(f"ERROR: Topic {topic_id} not found!")
                return {"error": "Topic not found"}
            # Assign MASTER_REC_ID as the Topic ID
            new_post = Post(
                master_id=7,  # topic_id,  # Matches MASTER_ID
                master_rec_id=topic.id,  # 7,  # topic.id,  # Ensures MASTER_REC_ID is set!
                author=author,
                content=content,
                created_at=datetime.utcnow()
            )
            session.add(new_post)
            session.commit()
            print(f"Post created successfully! ID: {new_post.id}, MASTER_REC_ID: {new_post.master_rec_id}")
            return {"message": "Post Created", "id": new_post.id, "master_rec_id": new_post.master_rec_id}
    except Exception as e:
        print(f"ERROR: Failed to create post - {e}")
        return {"error": str(e)}


def get_all_topics(item):
    item.open()
    return [{"id": t.id.value, "title": t.title.value} for t in item]


def get_posts_by_topic(item, topic_id):
    with Session(engine) as session:
        print(f"Fetching posts for topic {topic_id}...")
        session.expire_all()  # Force refresh
        # Debugging log
        print(f"Querying posts where MASTER_ID = {topic_id}")
        posts = session.query(Post).filter(Post.master_rec_id == topic_id).all()
        results = [
            {
                "id": p.id,
                "author": p.author,
                "content": p.content,
                "created_at": p.created_at.strftime("%Y-%m-%d %H:%M:%S")
            }
            for p in posts
        ]
        print(f"Returning posts for topic {topic_id}: {results}")
        return results


def create_topic(item, title):
    item.append()
    item.title.value = title
    item.created_at.value = datetime.now()
    item.post()
    item.apply()
    return {"message": "Topic Created", "id": item.id.value}

The two examples below demonstrate how to display automatically generated UI data grids for the forum database tables above, on separate task pages:

function on_view_form_created(item) {
    $('#content').empty();
    $('#content').append($('<div id="table">'));
    let c = task.details.posts.copy();  // task.details.items[0] // task.details.task.posts.copy();
    c.create_table($('#table'));
    c.open();
}


function on_view_form_created(item) {
    $('#content').empty();
    $('#content').append($('<div id="table">'));
    let c = task.journals.topics.copy();   // task.details.items[0] // task.details.task.posts.copy()
    c.create_table($('#table'));
    c.open();
}

The way you install Jam.py is:

python3 -m venv NEWFOLDERNAME
cd NEWFOLDERNAME
source bin/activate
pip install jam.py
python3 bin/jam-project.py
python3 server.py 9003          # choose any preferred port

To import an existing sqlite db and install a newer sqlalchemy version, together with Jam.py's sqlalchemy version:

rm -rf /root/NEWFOLDERNAME/lib/python3.8/site-packages/jam/third_party/sqlalchemy


pip install --force-reinstall sqlalchemy==2.0.37
python3 -c "import sqlalchemy; print(sqlalchemy.__version__)"

Edit the server_classes.py file:

nano /root/NEWFOLDERNAME/lib/python3.8/site-packages/jam/server_classes.py

Find this line:

from .third_party.sqlalchemy.pool import NullPool, QueuePool

Replace it with:

from sqlalchemy.pool import NullPool, QueuePool

Run the server again:

python3 server.py 9003

WHEN INITIALIZING THE PROJECT (at yoursite:9003/builder.html), CONFIGURE THE *SAME PROJECT NAME* (lowercase), BUT THE *NEW DATABASE NAME*

NOTE: If using SQLite, and importing a project, edit task.dat in the zip file, and change:

DATABASE_URL = \"sqlite:////root/ORIGIAL_FOLDER_NAME/ORIGINAL_DB_FILENAME

to

DATABASE_URL = \"sqlite:////root/NEWFOLDERNAME/NEW_DB_FILENAME  (filename on new server)

Be sure to save that edited task.dat file back into the zip file

The full Jam.py documentation is available at https://jam-py.com/docs/contents.html . A downloadable PDF version is available at https://jampy-docs.readthedocs.io/_/downloads/en/latest/pdf . An additional helpful guide is available at https://jampy-application-design-tips.readthedocs.io/en/latest and https://jampy-application-design-tips.readthedocs.io/_/downloads/en/latest/pdf . The official github repository, with a variety of example application links, is available at https://github.com/jam-py/jam-py .

The following summary of the Jam.py documentation explains how to work with the no-code and built-in database functionalities (ORM) on both the front-end and back-end:

Jam.py Framework Overview & Essential Concepts 1. Introduction Jam.py is a low-code, open-source Python framework for building database-driven web applications. It follows the task tree model, where objects (catalogs, journals, reports) interact with an SQL database. The framework supports lookup fields, filters, CRUD operations, and event-driven programming on both the client and server.

2. Building Your First Project 2.1 Creating a Catalog Open Application Builder. Add a new Catalog (e.g., Customers) with fields: Firstname (TEXT, 30 chars) Lastname (TEXT, 30 chars, Required) Phone (TEXT, 20 chars) Click OK to generate the corresponding SQL table. 2.2 Creating a Journal Journals store transactional data linked to catalogs.

Add a Journal (e.g., Contacts). Define fields: Contact date (DATETIME, default: CURRENT DATETIME) Notes (TEXT) Add a lookup field: Customer (references Customers catalog via Lastname). Click OK. Contacts now links to Customers, enabling relational data management. 3. Lookup Fields & Lists 3.1 Lookup Fields (Foreign Keys) A lookup field allows linking between tables. For Contacts, the Customer lookup field:

Stores the CustomerID Displays Lastname instead of the ID Enables typeahead/autocomplete Additional lookup fields (Firstname, Phone) can be master-linked to Customer to avoid redundant table joins.

3.2 Lookup Lists (Dropdowns) For predefined choices, use lookup lists:

Create a lookup list (e.g., Status). Add values: 1 - New 2 - In Progress 3 - Completed Assign Status as a lookup field in Contacts, using the list. 4. Forms & UI Customization 4.1 Default Forms Jam.py auto-generates:

View Forms (display records) Edit Forms (CRUD operations) Filter Forms (search data) 4.2 Customizing Forms Use Edit Form Dialog to reorder/hide fields. Define default values (CURRENT DATETIME for Contact date). Enable Typeahead for lookup fields. 4.3 Using Bootstrap Jam.py integrates with Bootstrap for responsive layouts:

Use tabs to organize fields. Define grid layouts for structured form design. 5. Data Handling & CRUD Operations 5.1 Dataset Basics Each catalog/journal operates as a dataset.

Call open() to load records. Use .next() to iterate rows. Example (Python server-side):

def get_customers(customers):
    customers.open()
    for c in customers:
        print(c.firstname.value, c.lastname.value)

5.2 Editing Data Operations change dataset state:

edit(), append(), insert() → Modify records. post() → Save changes. apply() → Write to database. Example (Client-side JavaScript):

function update_customer(customers) {
    customers.open();
    customers.edit();
    customers.firstname.value = "John";
    customers.post();
    customers.apply();
}

6. Filters & Querying Data 6.1 Defining Filters Filters refine data in UI without writing SQL. Example filter (invoicedate__ge):

invoices.filters.invoicedate1.value = datetime.now() - timedelta(days=30)
invoices.open()

6.2 Query Options Use .set_where() to filter programmatically:

invoices.set_where(invoicedate__ge=datetime.now())
invoices.open()
Supported conditions:

Operator SQL Equivalent eq = ne <> gt > lt < contains LIKE '%val%'

7. Reports & Exporting Data 7.1 Generating Reports Use LibreOffice templates. Call .print() on report items. Example:

task.customers_report.print()

7.2 Exporting to CSV Jam.py supports exporting datasets:

task.customers.export_to_csv("customers.csv")

8. Client & Server Logic 8.1 Event Handling Jam.py is event-driven. Common hooks:

on_view_form_created (UI customization) on_apply (Custom save logic) on_open (Modify queries dynamically) Example (on_apply event):

def on_apply(item, delta, params):
    for d in delta:
        if d.total.value > 100:
            d.vip.value = True  # Flag as VIP customer

8.2 Running Server Methods Call server functions from the client:

task.customers.server("my_server_function", arg1, arg2)

9. Security & User Authentication 9.1 Safe Mode If enabled, users must log in. Permissions restrict read/write access. 9.2 Enforcing Roles Assign roles:

def on_created(task):
    task.users.open()
    task.users.role = "Admin"

10. Deployment 10.1 Running Locally

python server.py

10.2 Hosting Deploy with:

Gunicorn (Linux) IIS (Windows) Docker (Containerized apps)

Here is some more useful summarized information from the Jam.py documentation:

1. Task Tree & Object Model Jam.py task tree organizes application elements. A task contains:

Catalogs (Data tables) Journals (Transactional logs) Details (Child tables) Reports (Output generation) Each element has attributes (field_name, lookup_value) and methods (open(), apply()).

Example: Accessing Objects in the Task Tree

def on_apply(item, delta, params):
    customers = item.task.catalogs.customers.copy()
    customers.open()
    for c in customers:
        print(c.firstname.value, c.lastname.value)

2. Creating & Managing Forms Jam.py auto-generates forms but supports customization.

Default Form Templates Located in index.html inside:

<div class="templates">
    <div class="default-edit">...</div>
    <div class="default-view">...</div>
</div>

Programmatically Modifying Forms

function on_edit_form_created(item) {
    item.edit_form.find("#ok-btn").on('click', function() {
        item.apply_record();
    });
}

3. Event Handling (Client & Server) Jam.py uses event-driven programming.

Common Client-Side Events

Event Name Description

on_page_loaded(task) Fires when the app loads. on_view_form_created(item) Customizes view forms. on_edit_form_created(item) Customizes edit forms.

Common Server-Side Events

Event Name Description

on_open(item) Modifies queries before execution. on_apply(item) Executes custom save logic. on_created(task) Initializes application state.

Example: Customizing Data Query (Server)

def on_open(item, params):
    item.set_where(status__eq='Active')

4. Advanced Data Operations Jam.py datasets behave like ORM objects, allowing:

Filtering (set_where) Sorting (set_order) Batch processing Filtering & Sorting Example

customers.set_where(country__eq="USA")
customers.set_order("lastname")
customers.open()
Iterating Over Data


for c in customers:
    print(c.firstname.value, c.lastname.value)

5. Lookup Fields & Foreign Keys Lookup fields link to other tables. Used for dropdowns and typeahead fields. Example: Defining a Lookup Field

customers.firstname.lookup_value = "John"

Example: Creating a Lookup Field Programmatically

def create_lookup_field():
    field = task.customers.add_field("region", "RegionLookup")
    field.lookup_item = task.regions
    field.lookup_field = "region_name"

6. Customizing UI with Bootstrap Jam.py integrates with Bootstrap for responsive layouts.

Example: Adding Tabs in an Edit Form

<ul class="nav nav-tabs">
    <li class="active"><a href="#tab1">General</a></li>
    <li><a href="#tab2">Details</a></li>
</ul>

Example: Dynamically Updating UI

function on_view_form_created(item) {
    $("#header-title").text("Customer Details");
}

7. Permissions & User Roles Jam.py enforces role-based access control.

Setting User Roles (Server-Side)

def on_created(task):
    task.users.open()
    task.users.role = "Admin"

Checking User Permissions

if task.user_info.role_name == "Admin":
    task.customers.open()

8. Deployment & Server Configuration Jam.py supports WSGI, Docker, and IIS.

Running Jam.py Locally

python server.py

Deploying with Gunicorn

gunicorn -w 4 server:app

Configuring Static Files in Production Modify server.py:

app = create_app(static_folder="static")

Understand the Task Tree – How items relate (e.g., journals, catalogs). Know Default UI Behaviors – How forms are auto-generated and customized. Handle CRUD Operations – Using open(), apply(), and event-driven workflows. Use Lookup Fields Correctly – How to define and retrieve related data. Support Client-Server Logic – Using JavaScript for UI, Python for backend. Implement Security & Roles – Restrict access based on user permissions. Deploy Correctly – Understand production setup.

Do you understand from this text how to structure code in Jam.py?

6. FINAL THOUGHTS

The productivity of Jam.py comes down to the fact that any software which accomplishes useful goals, needs to store data of some sort, somewhere, and Jam.py makes that whole part of the software engineering process dead simple to accomplish, with some really slick, lightweight but deeply capable no-code tooling. Even people with no software development experience can manage complex data, right away, without having to write any code - and all the no-code CRUD data management operations in Jam.py, all the professional quality UI grids and forms, all the unlimited combinatorial filter interfaces and multi column sort capabilities, the master-detail associations, audit trail histories, soft deletes, file handling features, authentication/authorization, etc., can all be implemented using your choice of the most common RDBMSs, along with mainstream web UI - so applications written with it are portable, performance is as fast as possible, reliability is rock solid, and scalability is built-in, out of the box. Any other software development tools which might be employed by data science professionals, analysts, IT personnel, etc., can connect directly with Jam.py application databases immediately, in any sort of common production environment. Since Jam.py is built from the ground up on mainstream database systems and web UI, it leverages some of the most deeply optimized, fully vetted, capable software tools in existence - the result of decades of stress-tested engineering work, in the hardest critical application settings where computing of any sort takes place. Jam.py wrangles database schema creation, management and connectivity requirements, in a way that takes virtually no effort, all accomplished in a uniquely elegant, painless, and practical way. Add to that a world class report generation system, which enables non-technical users to lay out report designs, to present data output in a wide variety of commonly useful, consumable formats (CSV, XLSX, PDF, ODF, HTML, etc.) - again without having to write code, and you’ve got a system which beats the productivity of other mainstream frameworks.

The real power of Jam.py comes from its extensibility - how easily the simple core database and UI features can be combined with custom code solutions, to build any imaginable software. Any functionality, UI feature, or computational capability built into Jam.py, can be extended with pure code in Python on the server and HTML/CSS/JS/jQuery/Bootstrap on the front-end. There are no better established, larger ecosystems of libraries and connectivity tooling than those of Python on the back-end, and the flavor of web UI tooling included in Jam.py’s front-end. Billions of hours of profoundly successful and intelligently engineered human work are enshrined in the reusable libraries and tools of the Python ecosystem. Aside from being the most popular programming language used in many fields (science, web development, etc.), Python also holds a special position in the development and deployment of AI tools, because it’s baked into CUDA and other tools which are at the heart of enabling the most world-changing technological AI advancements. And because there are billions of pages of Python and web UI code written and available online, large language AI models are deeply fluent, and more capable of instantly generating code solutions with those language ecosystems, than any others. There are also endless visual builders available for generating web UI, including many free and open source tools, and in-browser tools which can be injected directly into the Jam.py workflow.

But that's not it. Jam.py wraps up all required tooling into the simplest possible workflow - similar in concept to other massively productive integrated development environments, but without any of the typical size and complexity. All the work needed to build any part of a Jam.py app can happen directly in the tiny built-in IDE, which can be accessed on any device with a browser, with no other toolchain components required for most work. And the whole server system takes less than a minute to install on any common OS, on any common hardware (even low powered mobile devices and thin clients can run it). No install is ever required to run/update Jam.py web applications on any client machine. The whole framework is utterly tiny and simple compared to most heavy end-to-end software development solutions, and it requires only ubiquitously available infrastructure tooling, which is typically already installed in most mainstream computing environments (any version of Python on the server, and any mainstream browser on the clients). Since Jam.py is built on only the most ubiquitously used computing platforms, it’s easy to implement anywhere.

Importantly, every part of Jam.py is fully open source (BSD), so there's no worry about vendor lock-in of any kind. All the skills needed for Jam.py development are portable to many other high-level Python and JS software development platforms.

Jam.py is an extraordinarily powerful toolkit, but at the same time, using it is one of the easiest, most enjoyable software development experiences imaginable. It's so simple and fun to use that it feels like cheating. Anyone with basic computer skills can learn to build a database application in an afternoon with Jam.py, and that database can be used as the foundation of software which enables any computing task which modern technology is currently capable of completing. Jam.py combines the most powerful and pervasive tools in the modern tech landscape, connecting database, front-end and back-end code in the simplest ways possible, to form a serious professional software development framework which can be used to complete the most demanding production projects.