4D Blog

Home Product Give AI to a 30 years old 4D application

Give AI to a 30 years old 4D application

December 3, 2025

Product

In just a couple of years, Artificial Intelligence has gone from an emerging trend to an essential component of modern software. ChatGPT, Grok, Gemini, and other AI assistants now play a major role in everyone’s daily life, both professionally and personally.
That’s why 4D 21 introduces 4D.Vectors and 4D AI Kit: to give 4D developers simple, effective tools to add AI-powered features to their applications.
We’ve already shared many examples, tutorials, and webinars about AI, but I recently wondered: what would it take to bring AI into a 30-year-old 4D application?
Could you simply ask such an app for your top 10 customers and instantly get a nice chart back?

Well, guess what? It turned out to be so straightforward that it deserves its own blog post.

Let’s start with the project sources link on Github and a quick demo video

Project sources

 

The 4D Invoice application

The 4D Invoice application has been available in the 4D Depot for quite some time. It serves as a solid example of how to manage products, customers, and invoices in a clean and structured way. Although it already uses project mode (a requirement for taking advantage of the latest 4D AI features) it does not yet leverage DataClasses, and its forms predate the more recent Form class. The codebase is fairly large, uses generic design patterns, and includes several non-trivial business rules.

For this experiment, my goal was to add a new form enabling AI-powered interaction with the application. I wanted users to be able to converse with an AI assistant and ask anything from simple questions to more complex analytical queries. In real-world usage, this would instantly give a rather “old-school” application a modern feel, aligned with today’s AI-driven user expectations. Quite exciting indeed!

Another objective was to avoid making any changes to the data structure, and not alter the existing UI mechanisms. In other words, the AI upgrade had to be highly flexible, minimally intrusive, and ideally reusable in other legacy applications.

As a consequence, this upgrade does not rely on 4D.Vectors or embeddings. We’ll come back to this later, but it’s worth emphasizing now: bringing AI into your 4D app does not necessarily require embeddings or semantic search. They are separate concepts.

Getting prepared

At 4D, we’ve already shared several AI-based examples and webinars, including recent demonstrations on building a RAG system using tool calling. For this project, I decided to extract the chat UI form from the People & Skills demo and make it more generic and reusable.

The chat UI consists of the following components:

  • The AIChat form, along with its form class formAIChat.4dm. This form provides:
    • a simple text input for the user’s prompt
    • a web area that displays the conversation with the model
  • The AI_ChatWithTools class, responsible for:
    • instantiating an AI bot using the 4D AI Kit’s chat helper, based on the inference server and model configured in Resources/AIProvider.json
    • loading the tools defined in Resources/AITools.json
    • hosting the implementation of those tools

The class runs entirely in streaming mode so the user gets a ChatGPT-like experience, with responses appearing progressively.

  • Conversation rendering is handled by the ChatHTMLRenderer singleton.

It takes the collection of messages stored in the AIBot and turns them into HTML using the templates located in the Resources folder:

    • chat-template.html
    • chat-template.css
    • tool-icon.svg

 

I’ll be honest: the HTML rendering was essentially “vibe-coded” with the help of VS Code’s GitHub agent and Claude Sonnet 4.5. The most challenging part was ensuring proper rendering during streaming, especially for elements such as tables, tool calls, and charts.

AI Tool calling

As you probably know, an AI model does not inherently know anything about your data. It only gains access to it through tools that you explicitly provide. In this section, I’ll explain which tools I implemented and how they work.

tool_getProducts, tool_getClients, tool_getInvoices, tool_getInvoiceLines

All these tools are responsible for querying the database. Each performs a simple ORDA query based on the fields requested by the model.

Of course there’s a vast area for improvements in these tool functions. Thanks to 4D 21 AI native features we could imagine a semantic search using 4D.Vector and embeddings to boost Clients retrieval. As this would mean deeper changes within the structure, I chose a simpler path.

Let’s break down tool_getClients, as the same pattern applies to all the other “getter” tools:

Function tool_getClients($input : Object) : Object

This function receives an object as input and returns an object as output. The input describes search parameters; the output contains the resulting data.

Input validation

var $validation; $returnObject : Object	
var $entities : cs.CLIENTSSelection:=ds.CLIENTS.all()

$validation:=JSON Validate($input; This._getToolArgumentsSchema(This._functionName(Call chain)))
If (Not($validation.success))
	return {error: "Could not validate input parameters against JSON Schema, call the tool again with proper input parameters"}
End if 

The first step is validating that the input matches the schema defined in AITools.json.

Most modern models will produce well-structured tool calls, but depending on the model, validation is still a good safety practice.

If validation fails, the tool returns a simple object containing an error property. It is important to know that the model reads this value, so feel free to be explicit about what went wrong.

Setting up default input values

$input.ID:=($input.ID) || "any"
$input.Name:=($input.Name) || "@"
$input.Contact:=($input.Contact) || "@"
$input.Total_Sales:=$input.Total_Sales || {}
$input.Total_Sales.min:=$input.Total_Sales.min || 0
$input.Total_Sales.max:=$input.Total_Sales.max || 9999999
$input.orderBy:=($input.orderBy) || {}
$input.orderBy.field:=($input.orderBy.field) || "Name"
$input.orderBy.order:=($input.orderBy.order) || "asc"
$input.top:=($input.top) || This.defaultTop
$input.countOnly:=($input.countOnly) || False

Defaults ensure safe ORDA queries, but they also have an interesting side benefit: writing a full JSON schema in AITools.json can be tedious. Instead, I let GitHub Copilot infer the schema from the code and default structure and the result was excellent. Copilot produced an accurate schema definition based solely on the logic above.

Parameters to note

  • orderBy and top reduce the output size (and therefore token usage).
  • countOnly allows the model to request only record counts without retrieving data.

 

Practical examples:

  • If the model needs the number of clients → countOnly = true
  • If it wants the top 5 customers → top = 5 and orderBy: {field: “Total_Sales”, order: “desc”}

 

Initializing the output Object

$returnObject:={}
$returnObject.form:="Clients"
$returnObject.dataClass:="CLIENTS"
$returnObject.counts:={}
$returnObject.counts.total:=$entities.length
  • form indicates which UI form can be used to display results – we’ll see that later.
  • dataClass identifies the involved dataclass, important for querying across several tables – we’ll see that also later.
  • counts.total is the number of records before any filter is applied.

 

Executing the query

If ($input.ID#"any")
	$entities:=$entities.query("ID = :1"; $input.ID)
End if 

$entities:=$entities.query("Name = :1 and Contact = :2 and Total_Sales >= :3 and Total_Sales <= :4 order by "+$input.orderBy.field+" "+$input.orderBy.order; $input.Name; $input.Contact; $input.Total_Sales.min; $input.Total_Sales.max)

This part is straightforward and could certainly be made more generic, but that wasn’t my focus here.

Finalizing the result

$returnObject.counts.totalFiltered:=$entities.length
If ($returnObject.counts.totalFiltered>$input.top)
	$entities:=$entities.slice(0; $input.top)
End if 

$returnObject.counts.totalSent:=$entities.length
$returnObject.entities:=($input.countOnly) ? [] : $entities.toCollection("ID, Name, Contact, City, State, Country, Discount_Rate, Total_Sales, Comments")

return $returnObject

Breakdown

  • counts.totalFiltered: number of records after filtering
  • counts.totalSent: number actually returned (after applying top)
  • entities: the final data collection (or empty if countOnly = true)

 

You’ll notice that the tool controls exactly which attributes are sent to the model. Striking the right balance is important.

What to consider

  • Conversation quality: more data → better insights
  • Performance: more data → longer processing
  • Context window: local models especially can degrade when overloaded
  • Costs: more tokens → higher usage on cloud inference servers
  • Confidentiality: always consider GDPR and data sensitivity when choosing fields

 

The bottom line

Whatever object the tool returns, 4D AI Kit transparently serializes it and delivers it to the model. Pretty convenient, right?

 

Cross-table Querying and the early System Prompt

Another interesting discovery during this project was that the model did not automatically understand the relationships between tables. For example, prompts such as:

Give me the top 5 customers, their highest invoice, and their most ordered product

didn’t always produce reliable results.

The issue was simple: the model did not realize that the ID in the Clients table should be used to query the Invoices table, or that invoice lines relate to products, and so on.

In other words, I needed to give the model some knowledge about the structure, but without going too far. My requirements were:

  • No hard-coded system prompt: I don’t want to manually update the prompt whenever the structure changes.
  • No full structure catalog: The model does not need the entire schema; only the relevant relationships.

 

To achieve that, I reused a very handy class from Thomas Maul: StructureInfo.4dm, extracting only relationships between tables.

From this, I generated my initial system prompt:

var $relations : Collection:=This._relationsInfos()	
$systemPrompt:="You are a helpful assistant. I need your help to answer questions data stored in my application.\n"+\	
	"**CONTEXT**\n"+\
	"The application stores data about invoices, products and clients"+\
	"In some cases, you'll need to cross query several tables (dataClasses) in order to answer."+\
	"To help you, here are the application relations between tables (dataClasses):\n"+\
	JSON Stringify($relations)+"\n"+\
	"**INSTRUCTIONS**\n"+\
	"Analyze questions and answer step by step.\n"+\
	"Use the tools at your disposal to answer everytime you think they are relevant.\n"+\
	"**FORMATING**\n"+\
	"Use HTML everytime.\n"+\
	"Use bullet lists and Tables everytime everytime necessary\n"+\
	"**IMPORTANT**\n"+\
	"When calling tools, always include all required arguments in valid JSON.\n"+\
	"Do not call a tool with empty arguments. If a value is missing, choose a reasonable default.\n"+\
	"Always double check tools results before answering. Especially when they rely on vector search. \n"+\
	"Indeed they may return results not matching with your search intention.\n"+\
	"When tool calling returns data not related with the initial question, or that you cannot use to answer,\n"+\
	"avoid detailing such results too much and stay short.\n"

By combining this relation metadata with the dataClass information returned by each tool, the model was finally able to understand how to chain tool calls across tables.

This dramatically improved the quality and accuracy of the model’s answers, especially for multi-table analytics such as:

top customers by revenue

or

products most frequently ordered

or

largest invoices per client

User experience: open a 4D form directly from the conversational UI

In my previous experiments with RAG, I often displayed results in a separate list box. This approach worked, but it came with several drawbacks:

  • I needed the model to provide, in a separate hidden section, the list of entity IDs to display in the listbox. This meant more tokens, slower responses, and extra HTML tricks (like hiding the ID list inside a commented block).
  • It required additional work to make the listbox generic enough to handle different entity types (Clients, Products, Invoices, …).
  • It limited the UI and felt less integrated in the application.

 

For this project, I wanted something simpler, more elegant, and more natural for the user.

So I adopted a new strategy: each time the AI mentions an entity, it generates a custom hyperlink that directly opens the corresponding 4D form, filtered on the selected entity.

This turned out to be surprisingly easy. I simply added the following instructions to the system prompt:

"**CUSTOM URL HANDLING**\n"+\	
"Tools responses give information about the form to open when available, the dataClass and entities ID\n"+\
"When you display any element coming from a tool response, you must use a custom url so that the user can open the corresponding form\n"+\
"Such custom url must follow the following syntax examples:\n"+\
"<a href=\"myapp://openform?form=Products&dataClass=PRODUCTS&entities=989511\">A single product</a>\n"+\
"<a href=\"myapp://openform?form=Invoices&dataClass=INVOICESS&entities=654KJY,6467HGS,79864JSD\">A list of invoices</a>\n"

Since my tools always return the appropriate form, dataClass, and the list of entities, the model has everything it needs to generate these links correctly. AI models are very good at this kind of structured output.

Handling the custom URLs in 4D

All I needed on 4D side was a small URL-filtering mechanism in the web area to intercept myapp://links and open the right form with the right entity selection:

Function webAreaEventHandler($formEventCode : Integer)
	var $formToOpen : Text
	var $entitySelectionToShow : 4D.EntitySelection
	var $queryObject : Object

	Case of 
		: ($formEventCode=On Load)
			ARRAY TEXT($filters; 0)
			ARRAY BOOLEAN($allowDeny; 0)
			APPEND TO ARRAY($filters; "myapp://*")  // Intercept all URLs starting with myapp://
			APPEND TO ARRAY($AllowDeny; False)  //Allow
			WA SET URL FILTERS(*; "Web Area"; $filters; $allowDeny)

		: ($formEventCode=On URL Filtering)
			$url:=WA Get last filtered URL(*; "Web Area")

			// Parse the URL to determine what to do
			Case of 
			: ($url="myapp://openform?@")
				$queryObject:=This.queryObjectFromUrl($url)
				If ($queryObject=Null)
					return 
				End if 

				If ($queryObject.entitiesCollection.length>0)
					$entitySelectionToShow:=ds[$queryObject.dataClass].query("ID in :1"; $queryObject.entitiesCollection)
					CALL WORKER("Generic"; "W_Generic"; $queryObject.form; True; $entitySelectionToShow)
				Else 
					CALL WORKER("Generic"; "W_Generic"; $queryObject.form; False)
				End if 
			End case 
	End case

The result

The end result is a very smooth user experience, where the conversational UI blends seamlessly with the rest of the 4D application.

Users can chat with the AI, explore data, and instantly open native 4D forms, all without altering the application’s core logic or structure.

A modern interaction model, sitting right on top of a classic 4D app… with almost no intrusion.

blank

An additional tool: tool_createInvoice

The sample database provided in the GitHub project included only a handful of invoices, not nearly enough to make interactions with the AI interesting. I needed more data.

Creating a custom form or writing yet another invoice-generation routine felt both tedious and irrelevant for the purpose of this demo. And then it hit me: I already had everything I needed.

  • A conversational UI.
  • An AI agent aware of my data model.
  • Knowledge of clients, products, prices, relationships…

So why not let the AI create invoices directly from the chat interface?

All I had to do was add a new tool: tool_createInvoice.

My approach was straightforward:

  • Implement a simple invoice-creation function inside AI_ChatWithTools.
  • Wrap the whole process in a transaction:
    • On error: rollback and send a clear error message back to the model.
    • On success: commit the invoice and return its details.
  • Carefully document the tool schema, especially its input parameters.
  • Then… let GitHub Copilot generate the JSON schema inside AITools.json.

 

The result?

I can now ask the AI to generate a brand-new invoice entirely on its own. It finds a relevant client, selects products, calculates totals, and calls the tool, all through the same conversational UI I built earlier.

It turned out to be a surprisingly elegant way to generate realistic test data.

blank

One interesting observation: some models (notably OpenAI GPT-4.1) tend to ask for confirmation, or at least sufficient input details, before actually creating an invoice… unless you explicitly instruct them not to.

blank

OpenAI messages HTML rendering

As mentioned earlier, this part was entirely vibe-coded. Reuse it, adapt it, or ignore it, whatever suits your needs. The idea is simple: take a collection of OpenAI messages and render them nicely in HTML.

But of course, our end-users always deserve more than a basic dump of text. So I added a few quality-of-life features:

  • A clean, structured layout for tool calls, including a visual indication while a tool is running.

 

  • A one-click “Copy” button to easily copy any assistant message and paste it into a document, whether it’s Microsoft Word, Google Docs, or an email.

This makes the whole conversational experience feel polished and professional, while still being quick and lightweight to implement.

blank

  • A copy button for each table, allowing the user to instantly extract the raw tabular data and paste it directly into an Excel-like spreadsheet.

This is extremely convenient when users want to manipulate or compare data outside the application without exporting anything.

blank

Charting capabilities directly inside the conversational UI

When conversing with an AI about invoices, clients, or products, users naturally expect charts. Rankings, comparisons, trends… visual insights are now part of any modern experience.

So I decided to bring inline chart rendering into the chat UI and it turned out to be surprisingly easy, thanks to a bit of vibe-coding with GitHub Copilot.

I implemented it in three steps:

1) Import Chart.js inside chat-template.html and extend my HTML rendering engine to recognize <chart>…</chart> blocks.

While the AI is streaming its response, a small CSS animation displays a chart placeholder, giving a polished UX feel.

2) Extend the system prompt to explicitly teach the AI how to request charts. I added instructions such as:

"**CHARTS**\n"+\	
"Create charts for rankings, comparisons, trends, or distributions. Format: <chart>{...JSON...}</chart>\n"+\
"Available types: bar, line, pie, doughnut, radar, polarArea. Always include:\n"+\
"- \"type\": chart type\n"+\
"- \"data.labels\": array of x-axis labels\n"+\
"- \"data.datasets\": array with \"label\", \"data\" (numeric array), \"backgroundColor\" (color array)\n"+\
"- \"options.responsive\": true\n"+\
"- \"options.plugins.title\": {\"display\": true, \"text\": \"Chart Title\"}\n"+\
"- \"options.scales.y.beginAtZero\": true (for bar/line charts)\n"+\
"Use distinct vibrant colors (e.g., #4caf50, #2196f3, #ff9800, #e91e63, #9c27b0). Set \"legend.display\" to false for single datasets, true for multiple.\n"

3) Let the AI generate chart definitions in JSON, which the web area renders instantly using Chart.js.

blank

The result is genuinely impressive: the model produces meaningful visuals directly inside the conversation flow, enriching the experience dramatically.

That said, I wouldn’t rely entirely on the model for complex calculations. For production usage, it would be wise to add a series of tools dedicated to:

  • totals and cumulation
  • grouping and aggregations
  • time-based summaries
  • period comparisons
  • KPI extraction

 

4D would perform the hard, accurate computations, and the AI would simply request them and draw the charts based on precise, validated numeric data.

A perfect combination of 4D reliability and AI-powered UI.

Conclusion

So… what now?

We essentially took a somewhat old-fashioned business application and infused it with a modern, conversational, AI-powered experience.

But seeing it simply as “yet another way to fetch data” would be missing the point entirely.

Sure, you don’t need a conversational UI to get “the top 5 customers by total sales in 2025”.

A basic screen with a couple of filters can do that.

But very quickly, you realize this is something else.

This is a new way of exploring your data.

With one single form, the chat interface, users can ask anything: invoices, products, clients… whether they want a table, a chart, or both.

This eliminates the need to build countless specialized screens for slightly different reporting needs.

And once you see that, you also understand that this goes far beyond querying and displaying data.

The real value is not in answering “show me this”.

The real value is in answering “help me understand this”.

Your prompt should no longer be:

Draw a chart comparing 2024 and 2025

but instead:

Please provide a detailed sales report for 2025, including total sales, top 5 products by revenue, top 5 clients by sales, sales trend over the year, and unpaid invoices. Show relevant charts and tables, and explain any notable trends, opportunities, and risks. Also, compare product performance against 2024 and suggest actionable insights to improve sales and cashflow. Include performance review per client and per product with key metrics. I want both figures and explanations & actionable insights. Your report will include comparisons between 2024 and 2025. The goals of such report are: how to boost sales, which clients to target, how to increase product performance.

And this is the kind of answer you – and your users – will get:

blank

A single conversational interface, seamlessly integrated into your 4D application, capable of insights, reasoning, visualization, creation…

This isn’t a feature.

It’s a step toward a new generation of business apps.

How to reuse this in your own project?

If reading this post and playing around with the app raised an interest, you can simply:

  • Copy the following files into your project:
    • Classes
      • AI_ChatWithTools.4dm
      • ChatHTMLRenderer.4dm
      • formAIChat.4dm
      • StructureInfo.4dm
    • Form
      • AIChat
    • Resources assets
      • AIprovider.json
      • AITools.json
      • chat-template.html
      • chat-template.css
      • tool-icon.svg
    • Make the necessary adaptations
      • System prompt and tools implementation in AI_ChatWithTools.4dm
      • Subsequent tools definition in AITools.json (just use AI for this!)
      • Adapt the form opening to your application in formAIChat.webAreaEventHandler()
      • AI provider settings in AIprovider.json

 

And that’s it!

I’ll finish with a couple of additional remarks:

In this example, all tool logic is implemented within the AI_ChatWithTools class. I did so because my goal was to be as less intrusive as possible, and because the project does not implement DataClasses. Depending of your code base architecture, a better practice could be to have such tools targeting DataClasses functions.

All this project works perfectly with OpenAI and gpt-4.1 model or superior. Working with a local model raises additional challenges that can be discussed in 4D forum!

 

Discuss

Tags 21, 4D AIKit, AI, AI Tool Calling, Artificial Intelligence, Vectors

Latest related posts

  • January 22, 2026

    Transform Static Documents into Actionable Knowledge with AIKit

  • January 22, 2026

    Deploy Fluent UI effortlessly in your 4D applications

  • January 21, 2026

    Searching Across Host Projects and Components in 4D

Mathieu Ferry
Mathieu Ferry
  • Deutsch
  • Français
  • English
  • Português
  • Čeština
  • Español
  • Italiano
  • 日本語

Categories

Browse categories

  • AI
  • 4D View Pro
  • 4D Write Pro
  • 4D for Mobile
  • Email
  • Development Mode
  • 4D Language
  • ORDA
  • User Interface / GUI
  • Qodly Studio
  • Server
  • Maintenance
  • Deployment
  • 4D Tutorials
  • Generic
  • 4D Summit sessions and other online videos

Tags

4D AIKit 4D for Android 4D for iOS 4D NetKit 4D Qodly Pro 4D View Pro 4D Write Pro 20 R10 21 Administration AI Artificial Intelligence Build application CI/CD Class Client/Server Code editor Collections Formula Google Listbox Logs Mail Microsoft 365 Network Objects OpenAI ORDA PDF Pictures Preemptive Programming REST Scalability Security Session Source control Speed Spreadsheet Tutorial UI User Experience vscode Web Word processor

Tags

4D AIKit 4D for Android 4D for iOS 4D NetKit 4D Qodly Pro 4D View Pro 4D Write Pro 20 R10 21 Administration AI Artificial Intelligence Build application CI/CD Class Client/Server Code editor Collections Formula Google Listbox Logs Mail Microsoft 365 Network Objects OpenAI ORDA PDF Pictures Preemptive Programming REST Scalability Security Session Source control Speed Spreadsheet Tutorial UI User Experience vscode Web Word processor
Subscribe to 4D Newsletter

© 2026 4D SAS - All rights reserved
Terms & Conditions | Legal Notices | Data Policy | Cookie Policy | Contact us | Write for us


Subscribe to 4D Newsletter

* Your privacy is very important to us. Please click here to view our Policy

Contact us

Got a question, suggestion or just want to get in touch with the 4D bloggers? Drop us a line!

* Your privacy is very important to us. Please click here to view our Policy