Unlock a world of possibilities! Login now and discover the exclusive benefits awaiting you.
By reading the Product Innovation blog, you will learn about what's new across all of the products in our growing Qlik product portfolio.
The Support Updates blog delivers important and useful Qlik Support information about end-of-product support, new service releases, and general support topics.
This blog was created for professors and students using Qlik within academia.
Hear it from your Community Managers! The Community News blog provides updates about the Qlik Community Platform and other news and important announcements.
The Qlik Digest is your essential monthly low-down of the need-to-know product updates, events, and resources from Qlik.
The Qlik Learning blog offers information about the latest updates to our courses and programs, as well as insights from the Qlik Learning team.
Qlik Answers transforms unstructured data into clear, AI-powered insights. Today, I'll show you how to integrate Qlik Answers directly into your web app using the newly released Knowledgebases API and Assistants API.
In this blog, we'll build a custom Football chat assistant from scratch powered by Qlik Answers.
We’ll leverage the Assistants API to power real-time Q&A while the knowledge base is already set up in Qlik Sense.
For those of you who prefer a ready-made solution, you can quickly embed the native Qlik Answers UI using qlik-embed:
<qlik-embed
ui="ai/assistant"
assistant-id="<assistant-id>"
></qlik-embed>
You can explore the ai/assistant parameters (and other UIs available in qlik-embed) on qlik.dev, or take a look at some of my previous blog posts here and here.
For full documentation on the Knowledgebases API and Assistants API, visit qlik.dev/apis/rest/assistants/ and qlik.dev/apis/rest/knowledgebases/.
Let’s dive in and see how you can take control of your Qlik Answers UI experience!
Before we start building our DIY solution, here’s a quick refresher:
Knowledgebases: Collections of individual data sources (like HTML, DOCX, TXT, PDFs) that power your Qlik Answers. (In our case, we built the KB in Qlik Sense!)
Assistants: The chat interface that interacts with users using retrieval-augmented generation (RAG). With generative AI in the mix, Qlik Answers delivers reliable, linked answers that help drive decision-making.
Step 1: Get your data ready
Since we already created our knowledge base directly in Qlik Sense, we skip the Knowledgebases API. If you’d like to build one from scratch, check out the knowledgebases API documentation.
Step 2: Configure your assistant
With your knowledge base set, you create your assistant using the Assistants API. This is where the magic happens: you can manage conversation starters, customize follow-ups, and more. Visit the assistants API docs on qlik.dev. to learn more
Step 3: Build Your Custom UI
Now, let’s look at our custom chat UI code. We'll built a simple football-themed chat interface that lets users ask questions related to the NFL. The assistant’s answers stream in seamlessly to the interface.
HTML:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Football Assistant</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<h4>Let's talk Football</h4>
<span class="header-span">You ask, Qlik answers.</span>
</div>
<div class="chat-body" id="chat-body">
<div class="message assistant">
<div class="bubble">
<p>Hey there, champ! Ask me anything.</p>
</div>
</div>
</div>
<div class="chat-footer">
<input
type="text"
id="chat-input"
placeholder="Type your Football related question..."
/>
<button id="send-btn">Send</button>
</div>
</div>
<script src="scripts.js"></script>
</body>
</html>
Frontend JS:
document.addEventListener("DOMContentLoaded", () => {
const chatBody = document.getElementById("chat-body");
const chatInput = document.getElementById("chat-input");
const sendButton = document.getElementById("send-btn");
// Append a user message immediately
function appendUserMessage(message) {
const messageDiv = document.createElement("div");
messageDiv.classList.add("message", "user");
const bubbleDiv = document.createElement("div");
bubbleDiv.classList.add("bubble");
bubbleDiv.innerHTML = `<p>${message}</p>`;
messageDiv.appendChild(bubbleDiv);
chatBody.appendChild(messageDiv);
chatBody.scrollTop = chatBody.scrollHeight;
}
// Create an assistant bubble that we update with streaming text
function createAssistantBubble() {
const messageDiv = document.createElement("div");
messageDiv.classList.add("message", "assistant");
const bubbleDiv = document.createElement("div");
bubbleDiv.classList.add("bubble");
bubbleDiv.innerHTML = "<p></p>";
messageDiv.appendChild(bubbleDiv);
chatBody.appendChild(messageDiv);
chatBody.scrollTop = chatBody.scrollHeight;
return bubbleDiv.querySelector("p");
}
// Send the question to the backend and stream the answer
function sendQuestion() {
const question = chatInput.value.trim();
if (!question) return;
// Append the user's message
appendUserMessage(question);
chatInput.value = "";
// Create an assistant bubble for the answer
const assistantTextElement = createAssistantBubble();
// Open a connection to stream the answer
const eventSource = new EventSource(
`/stream-answers?question=${encodeURIComponent(question)}`
);
eventSource.onmessage = function (event) {
if (event.data === "[DONE]") {
eventSource.close();
} else {
assistantTextElement.innerHTML += event.data;
chatBody.scrollTop = chatBody.scrollHeight;
}
};
eventSource.onerror = function (event) {
console.error("EventSource error:", event);
eventSource.close();
assistantTextElement.innerHTML += " [Error receiving stream]";
};
}
sendButton.addEventListener("click", sendQuestion);
chatInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
sendQuestion();
}
});
});
Backend node.js script:
import express from "express";
import fetch from "node-fetch";
import path from "path";
import { fileURLToPath } from "url";
// Setup __dirname for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Define port and initialize Express app
const PORT = process.env.PORT || 3000;
const app = express();
app.use(express.static("public"));
app.use(express.json());
// Serve the frontend
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "public", "index.html"));
});
// Endpoint to stream Qlik Answers output
app.get("/stream-answers", async (req, res) => {
const question = req.query.question;
if (!question) {
res.status(400).send("No question provided");
return;
}
// Set headers for streaming response
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
const assistantId = "b82ae7a9-9911-4830-a4f3-f433e88496d2";
const baseUrl = "https://sense-demo.us.qlikcloud.com/api/v1/assistants/";
const bearerToken = process.env["apiKey"];
try {
// Create a new conversation thread
const createThreadUrl = `${baseUrl}${assistantId}/threads`;
const threadResponse = await fetch(createThreadUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${bearerToken}`,
},
body: JSON.stringify({
name: `Conversation for question: ${question}`,
}),
});
if (!threadResponse.ok) {
const errorData = await threadResponse.text();
res.write(`data: ${JSON.stringify({ error: errorData })}\n\n`);
res.end();
return;
}
const threadData = await threadResponse.json();
const threadId = threadData.id;
// Invoke the Qlik Answers streaming endpoint
const streamUrl = `${baseUrl}${assistantId}/threads/${threadId}/actions/stream`;
const invokeResponse = await fetch(streamUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${bearerToken}`,
},
body: JSON.stringify({
input: {
prompt: question,
promptType: "thread",
includeText: true,
},
}),
});
if (!invokeResponse.ok) {
const errorData = await invokeResponse.text();
res.write(`data: ${JSON.stringify({ error: errorData })}\n\n`);
res.end();
return;
}
// Process and stream the response text
const decoder = new TextDecoder();
for await (const chunk of invokeResponse.body) {
let textChunk = decoder.decode(chunk);
let parts = textChunk.split(/(?<=\})(?=\{)/);
for (const part of parts) {
let trimmedPart = part.trim();
if (!trimmedPart) continue;
try {
const parsed = JSON.parse(trimmedPart);
if (parsed.output && parsed.output.trim() !== "") {
res.write(`data: ${parsed.output}\n\n`);
}
} catch (e) {
if (trimmedPart && !trimmedPart.startsWith('{"sources"')) {
res.write(`data: ${trimmedPart}\n\n`);
}
}
}
}
res.write("data: [DONE]\n\n");
res.end();
} catch (error) {
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
});
// Start the backend server
app.listen(PORT, () => {
console.log(`Backend running on port ${PORT}`);
});
Okay, that was a lot of code! Let’s break it down into bite-sized pieces so you can see exactly how our custom Qlik Answers chat interface works.
1. The HTML
Our index.html creates a custom chat UI. It sets up:
A chat footer with an input field and a send button for users to type their questions.
2. The Frontend JavaScript (scripts.js)
This script handles the user interaction:
Appending messages: When you type a question and hit send (or press Enter), your message is added to the chat window.
Creating chat bubbles: It creates separate message bubbles for you (the user) and the assistant.
Streaming the answer: It opens a connection to our backend so that as soon as the assistant’s response is ready, it streams into the assistant’s bubble. This gives you a live, real-time feel without any manual “typing” effect.
3. The Node.js Backend (index.js)
Our backend does the heavy lifting:
Creating a conversation thread: It uses the Assistants API to start a new thread for each question.
Invoking the streaming endpoint: It then sends your question to Qlik Answers and streams the response back.
Processing the stream: As chunks of text come in, the backend cleans them up—splitting any concatenated JSON and only sending the useful text to the frontend.
Closing the stream: Once the complete answer is sent, it signals the end so your chat bubble doesn’t wait indefinitely.
4. How It All Connects
When you send a question:
Your message is displayed immediately in your custom chat bubble.
The backend creates a thread and requests an answer from Qlik Answers.
The response is streamed back to your UI in real time, making it look like the assistant is typing out the answer as it arrives.
P.S: this is just a simple example to introduce you to the new Answers APIs and show you how to get started using them, you'll need to double check limitations and adhere to best practices when using the APIs in a production environment.
You can find the full code here:
https://replit.com/@ouadielimouni/QA-Test-APIs#public/index.html
Happy coding - and, Go Birds 🦅!
Edited December 5th: identified upgrades leading to complications with extensions
Edited December 6th: added workaround for extension complication
Edited December 10th: added CVEs (CVE-2024-55579 and CVE-2024-55580)
Edited December 12th, noon CET: added new patch versions and visualization and extension fix details; previous patches were removed from the download site
Hello Qlik Users,
New patches have been made available and have replaced the original six releases. They include the original security fixes (CVE-2024-55579 and CVE-2024-55580) as well as QB-30633 to resolve the extension and visualization defect.
If you continue to experience issues with extensions or visualizations, see QB-30633: Visualizations and Extensions not loading after applying patch.
Security issues in Qlik Sense Enterprise for Windows have been identified, and patches have been made available. Details can be found in Security Bulletin High Severity Security fixes for Qlik Sense Enterprise for Windows (CVE-2024-55579 and CVE-2024-55580).
Today, we have released six service releases across the latest versions of Qlik Sense to patch the reported issue. All versions of Qlik Sense Enterprise for Windows prior to and including these releases are impacted:
No workarounds can be provided. Customers should upgrade Qlik Sense Enterprise for Windows to a version containing fixes for these issues. November 2024 IR, released on the 26th of November, contains the fix as well.
This issue only impacts Qlik Sense Enterprise for Windows. Other Qlik products including Qlik Cloud and QlikView are NOT impacted.
All Qlik software can be downloaded from our official Qlik Download page (customer login required). Follow best practices when upgrading Qlik Sense.
The information in this post and Security Bulletin High Severity Security fixes for Qlik Sense Enterprise for Windows (CVE-2024-55579 and CVE-2024-55580) are disclosed in accordance with our published Security and Vulnerability Policy.
The Security Notice label is used to notify customers about security patches and upgrades that require a customer’s action. Please subscribe to the ‘Security Notice’ label to be notified of future updates.
Thank you for choosing Qlik,
Qlik Global Support
Slack has updated the deprecation date to November 12th, 2025 (originally March 11th).
Starting November 12th, 2025, Slack will enforce changes in their APIs affecting file uploads. To accommodate these breaking changes, we have introduced new blocks in the Slack connector for Qlik Application Automation.
What blocks are affected?
The Send Binary File block will be deprecated. Instead, use the Upload File to Channel block to upload binary files. If you still want to send a base64 encoded string, use the Send Text Based File block and configure the encoding parameter to base64.
The Upload File To Channel block and Send Text Based File block need to be updated to a new version. To perform this update, replace existing blocks with new blocks by dragging the blocks from the block library.
Any automation using affected blocks needs to be updated.
See Breaking changes for file support in the Slack connector: new blocks introduced for steps and details.
Thank you for choosing Qlik,
Qlik Support
Indian IT hiring landscape is at a pivotal juncture as it transitions from a year of decline towards a more hopeful future. The focus on specialised skills, particularly in AI and data science, combined with geographical shifts towards Tier 2 cities, indicates a transformation within the sector. While the IT hiring landscape in India in 2024 was marked by delayed onboarding and a decline in overall hiring activity, the outlook for 2025 appears promising with expectations of recovery and growth fuelled by improvements in economic conditions and technological advancements.
Several years ago, I blogged about how creating a synthetic dimension using ValueList allowed us to color dimensions in a chart. ValueList is commonly used where there is not a dimension in the data model to use, thus creating a synthetic one with ValueList. You can read more about ValueList in myprevious blog post. In this blog post, I am going to share how I used ValueList to handle omitted dimension values in a chart.
I recently ran into a scenario when creating visualizations based on survey data. In the survey, the participant was asked for their age as well as their age group. The ages were grouped into the following buckets:
Once I loaded the data, I realized that there were not participants for all the age groups, so my chart looked like the bar chart below. There was a bar and value for only the age groups that the participants fit in.
While I could leave the chart like this, I wanted to show all the age group buckets in the chart so that it was evident that there were no participants (0%) in the other age group buckets. In this example, the four age groups were consecutive, so it did not look odd to leave the chart as is but imagine if there were no participants in the 45-54 age bucket. The chart may look odd with the gap between 44 and 55.
I explored various ways to handle this. One way was to add rows to the respective table for the missing age group. This worked fine but I was not a fan of adding rows to the survey table that were not related to a specific participant. The option that I settled on was using ValueList to add the omitted age groups. While this option works well, it can lead to lengthy expressions for the measures. In this example, there were only seven age group buckets so it was manageable but if you had many dimensions values then it may not be the best option.
To update the bar chart using ValueList, I changed the dimension from
To
Then I changed the measure from
To
Using ValueList in the dimension created a synthetic dimension with each age group option that was included in the survey. Now I will see all the age buckets in the chart even if there were no participants that fell in the age group bucket. Since I am using ValueList for the dimension, I need to update the measure to use it as well. This is where a single line measure can become a lengthier measure because I need to create a measure for every value in the synthetic dimension, thus the nested if statement above. The result looks like this:
There are no gaps in the age buckets, and we can see all the age bucket options that were presented in the survey. I prefer this chart over the first bar chart I shared because I have a better understanding of the survey responses presented to the participants as well as the response they provided. I would be interested in hearing how others have handled similar scenarios.
Thanks,
Jennell
Technology leaders, practitioners and business leaders alike, all intrinsically recognize the value of data to inform decisions and actions to drive growth and operational efficiency within their organizations. However, many have historically struggled to align the scale and timing of their investments in technology with the expected business outcomes.
With the general availability of Qlik Talend Cloud in July 2024, we delivered a solution that effectively addresses this need and allows organizations to plan and execute data-driven business transformations with confidence.
Here is a quick overview of Qlik Talend Cloud's Packaging and Pricing.
Qlik's Dynamic Engine, natively integrated with Kubernetes (K8s), revolutionizes data processing with unmatched scalability, automation, and unified workflows.
Supporting on-premise, hybrid, and SaaS environments, it offers flexibility for modern enterprises. Key features include Kubernetes-driven scalability, seamless migration from Remote Engines, and automated updates via a built-in versioning system.
By optimizing resource allocation and unifying workflows across the Qlik Talend Cloud, Dynamic Engine delivers a future-ready solution for large-scale data integration and processing needs.
The new Navigation Menu object in Qlik allows for a flexible and easy way to add a menu to your Qlik Sense apps. Whether you're designing an app in Qlik Sense or embedding Qlik capabilities into a web platform, you should make this new object your go-to solution for organized and stylish navigation.
In this blog post, we'll explore into the new Navigation Menu object, its features, customization options, embedding capabilities, and key considerations for getting the most out of it. For a quick overview, check out the SaaS in 60 video below:
Up until now, navigation in your apps has been limited to the following options:
The new Navigation Menu object enhances your Qlik Sense app usability and makes it easier to achieve similar results faster tailored to your needs
Key Features:
You can toggle the "App Navigation bar > Navigation" or "Sheet Header" option to OFF in the UI settings and have the Navigation Menu object replace your traditional Sheets for a more minimalistic look.
👀 You can see the Navigation Menu object in action on the What’s New App:
https://explore.qlik.com/app/4911b1af-2f3c-4d8a-9fd5-1de5b04d195c
For developers looking to incorporate Qlik analytics into their web applications, the Navigation Menu object can save time when developing a custom sheet navigation. You can easily embed the navigation menu object and customize it to meet your needs. (Learn more here)
How to Embed the Navigation Menu
Here’s a simple example of embedding a horizontal navigation menu in your web app using Nebula.js.
You can read more about Embedding and access the full documentation on qlik.dev:
const nuked = window.stardust.embed(app, {
context: { theme: "light" },
types: [
{
name: "sn-nav-menu",
load: () => Promise.resolve(window["sn-nav-menu"]),
},
],
});
nuked.render({
type: "sn-nav-menu",
element: document.querySelector(".menu"),
properties: {
layoutOptions: {
orientation: "horizontal",
alignment: "top-center",
},
},
});
Advanced Customization
Using JSON properties, you can customize the navigation menu extensively:
These capabilities make the Navigation Menu a versatile tool for developers working on embedded analytics projects.
nuked.render({
type: "sn-nav-menu",
element: document.querySelector(".menu"),
properties: {
"layoutOptions": {
"drawerMode": false,
"hideDrawerIcon": false,
"orientation": "horizontal",
"layout": "fill",
"alignment": "top-center",
"separateItems": false,
"dividerColor": {
"color": "rgba(0,0,0,0.12)",
"index": -1
},
"largeItems": false,
"showItemIcons": false
},
"components": [
{
"key": "general"
},
{
"key": "theme",
"content": {
"fontSize": "18px",
"fontStyle": {
"bold": true,
"italic": false,
"underline": false,
"normal": true
},
"defaultColor": {
"index": 15,
"color": "#000000",
"alpha": 1
},
"defaultFontColor": {
"color": "#ffffff",
"alpha": 1
},
"highlightColor": {
"index": -1,
"color": "#3ba63b",
"alpha": 1
},
"highlightFontColor": {
"color": "#ffffff",
"alpha": 1
},
"hoverColor": {
"index": -1,
"color": "#ffa82e",
"alpha": 1
},
"borderRadius": "20px"
}
}
]
},
navigation: sheetObject.navigation,
});
While the Navigation Menu object is a fantastic addition, there are some key points to consider:

You can leverage section access to anonymize field values.

Allows users to get a view of the whole environment without breaching any data privacy rules.

Financial Controllers

Financial controllers are able to tell how well their own country is performing in comparison to others and can adjust their strategy accordingly.