AttilaVarga
Kilo Sage

Preamble:


Previously the co-author of this solution @Martin Virag wrote an article on how you can speed up diagramming with the help of AI . However, the approach detailed there have had one flaw. It did not run on ServiceNow!

Therefore we decided to build a custom application that allows you to generate diagrams by using Draw.io and an AI agent running on the now platform.

 

The solution consist a custom UI Builder page where we have introduced a custom Agent Component and a Draw.io integration.

 

In the next part of our article series, we would like to present one of the main components of our solution: the Draw.io integration. This feature allows users to create diagrams and save them in the ServiceNow database without leaving the platform.

 

Draw.io (diagrams.net)

Let’s start from a broader perspective. Draw.io does not really need an introduction: it is a free diagram creation tool, that can be used both in a web browser and as a desktop application. Most of the diagram types are supported.

The developers of the product also realized, that productivity increases when the tool can work together with other systems. This is achieved by embedding the application using an iFrame component. (Just let's think about the Confluence - Draw.io integration.) The communication between the parent window and the iFrame HTTP element is based on a technology, that may seem surprisingly old. Browsers have supported interaction between these two elements since the 2010s, using the JavaScript postMessage() function to send data and by listening to the "message" event to receive incoming information.

 

We took this feature of Draw.io as a basis when developing our solution.

 

Communication between Parent page and iFrame component

In the following section, we will go into the technical details through a ServiceNow-based example to build the knowledge, which is needed to understand the functions used in our solution. It is important to present this case using ServiceNow, because at the same time it also shows what kind of support the platform provides for this type of implementation.

 

A simple UI Page will be used in the example which is embedded into the iFrame on both Service Portal and Workspace layouts.

 

Build the UI Page

Let's start building a very simple the UI Page, using the most generic "Hello World" content:

 

AttilaVarga_0-1768816572710.png

 

Once the UI Page is done, need to embed it into an iFrame component, which is part of a portal widget.

 

AttilaVarga_1-1768816669603.png

 

The result so far looks like the following:

 

AttilaVarga_2-1768816712289.png

 

We need to do the same action in the UI Builder as well. First a page and a variant has to be created than the iFrame component with the reference page.

 

AttilaVarga_4-1768817021855.png

 

The result of our work can be seen below:

AttilaVarga_3-1768816945071.png

 

We are finished with the preparation. Now we will create a simple chat-like communication solution as an example.

The message content type is JSON. Only two attributes are used, one for the message content (msg) and the second one is the actual date (date).
As I mentioned earlier, postMessage() is used to send messages, and the message event is used to receive them.
Remember, the UI page is in an iFrame, so we have to use the postMessage function from the parent component. Otherwise, the parent cannot receive the message.
Another important element is the origin. This is a security setting used to check if the data comes from a valid source. If the check fails, the message will not be processed.

 

Let's take a look at the source code.

 

HTML code of the UI Page:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">

	<style>
        .container {
            max-width: 300px;
            display: flex;
            flex-direction: column;
            gap: 10px;
        }

        input, textarea {
            width: 100%;
            box-sizing: border-box;
        }

		button {
			width: 150px;
		}

        textarea {
            resize: vertical;
			height: 300px;
        }
    </style>

	<h3>Child element in iFrame</h3>

	<div class="container">
		<input id="chtInput" type="text" class="form-control" />
		<button onclick="sendMessageToParent(document.querySelector('#chtInput').value);" class="btn btn-default">Send to parent</button>
		<textarea id="chtArea" rows="7" class="form-control"></textarea>
	</div>
</j:jelly>

 

JavaScript code of the UI Page:

// This is the listener of incoming messages
window.addEventListener("message", (event) => {
    // The incoming data is a JSON, this is a built-in SN feature
    if (event.origin === window.location.origin) {
        let message = "Parent:\t(" + event.data.date + ") " + event.data.msg + "\n";
        document.getElementById("chtArea").value += message;
    } else {
        console.warn("The incoming message is ignored, because of invalid origin.");
    }
});

/**
 * This function is responsible for sending the message from iFrame to parent
 */
function sendMessageToParent(chtMessage) {
    let message = {
        msg: chtMessage,
        date: getCurrentDateTime()
    };
    // Set the origin to be on the safe side.
    let origin = window.parent.location.origin;
    window.parent.postMessage(message, origin);

    let pMessage = "Me:\t(" + message.date + ") " + message.msg + "\n";
    document.getElementById("chtArea").value += pMessage;
    document.getElementById("chtInput").value = "";
}

/**
 * Simple function in order to get the date in a correct format
 */
function getCurrentDateTime() {
    const now = new Date();

    const yyyy = now.getFullYear();
    const mm = String(now.getMonth() + 1).padStart(2, '0');
    const dd = String(now.getDate()).padStart(2, '0');

    const HH = String(now.getHours()).padStart(2, '0');
    const MM = String(now.getMinutes()).padStart(2, '0');
    const SS = String(now.getSeconds()).padStart(2, '0');

    return yyyy + "." + mm + "." + dd + ". " + HH + ":" + MM + ":" + SS;
}
 
The UI page is ready, and we need to embed it into Service Portal and UI Builder pages. I will give a detailed description of the solution to make it as clear as possible.

 

SP widget source

HTML code:

<div>
  <div class="parent-elem">
    <div class="iframe-elem">
      <iframe id="testIFrame" src="/iframe_child_page" class="iframe-test" />
    </div>
    <div>
    </div>
    <div class="container">
      <h4>
        Parent element
      </h4>
      <input id="chtInput" type="text" class="form-control" ng-model="c.chatMsg" />
      <button ng-click="c.sendMessageToChild();" class="btn btn-default">Send to child</button>
      <textarea id="chtArea" rows="7" class="form-control" ng-model="c.chatArea"></textarea>
    </div>
  </div>
</div>

 

Client controller:

api.controller=function($scope) {
	/* widget controller */
	var c = this;

	c.chatMsg = "";
	c.chatArea = "";

	// This is the message listener
	window.addEventListener('message', function (event) {
		$scope.$apply(function () {
			
			// This is important for security reason
			if (event.origin === window.location.origin) {
				let pMessage = "Child:\t(" + event.data.date + ") " + event.data.msg + "\n";
				c.chatArea += pMessage;
			}
			else {
				console.warn("The incoming message is ignored, because of invalid origin.");
			}
		});
	});

	// Sending message to iFrame
	c.sendMessageToChild = function() {
		console.log(c.chatMsg)

		const iframe = document.getElementById('testIFrame');

		let message = { 
			msg: c.chatMsg,
			date: getCurrentDateTime()
		};

		iframe.contentWindow.postMessage(
			message,
			window.location.origin
		);

		c.chatMsg = "";
		let pMessage = "Me:\t(" + message.date + ") " + message.msg + "\n";
		c.chatArea += pMessage;
	};

	/**
	 *	Get date information in a correct format
	 */
	function getCurrentDateTime() {
		const now = new Date();

		const yyyy = now.getFullYear();
		const mm = String(now.getMonth() + 1).padStart(2, '0');
		const dd = String(now.getDate()).padStart(2, '0');

		const HH = String(now.getHours()).padStart(2, '0');
		const MM = String(now.getMinutes()).padStart(2, '0');
		const SS = String(now.getSeconds()).padStart(2, '0');

		return yyyy +"."+ mm +"."+ dd +". "+ HH +":"+ MM +":"+ SS;
	}
};

 

UI Builder page

 

The structure of the UI Builder page is also simple, but the configuration is slightly more complex than the SP widget, so let’s go through it step by step.

The following state parameters are required:

  • chatText: this variable stores the current text

  • iFrameContent: this value is sent to the iFrame component

  • textAreaContent: this contains the chat message shown on the screen

  • pageOrigin: this setting is important for secure message communication (I have already explained this attribute.)

AttilaVarga_0-1768833622212.png

Three client scripts are responsible for the chat feature:

 

1. Process incoming message

 

The iFrame component has an out-of-the-box event listener that can be used to listen to incoming messages. Our script is executed when the event is fired.

 

AttilaVarga_1-1768834040862.png

 

function handler({api, event, helpers, imports}) {
    console.log(event);

    if (event.payload.data.msg) {
        let pMessage = "Child:\t(" + event.payload.data.date + ") " + event.payload.data.msg + "\n";
        api.setState("textAreaContent", api.state.textAreaContent + pMessage);
    }
}

 

2. Send message to iFrame element

 

This script is responsible for sending data and is triggered when the button is clicked. The UI Builder includes a built-in messaging feature for the iFrame component, which requires the use of the Data and Target origin parameters.

The message is sent automatically as soon as the value of the Data parameter changes.

 

⚠️ Important information: while the postMessage() function itself can handle objects of any type, the UI Builder iFrame component only works with JSON. In the case of Draw.io, this limitation causes an issue, which will be discussed later.

function handler({api, event, helpers, imports}) {

    let message = {
        msg: api.state.chatText,
        date: getCurrentDateTime()
    };
    console.log(message);
    api.setState("iFrameContent", JSON.stringify(message));

    let pMessage = "Me:\t(" + message.date + ") " + message.msg + "\n";
    api.setState("textAreaContent", api.state.textAreaContent + pMessage);
    api.setState("chatText", "");

    function getCurrentDateTime() {
        const now = new Date();

        const yyyy = now.getFullYear();
        const mm = String(now.getMonth() + 1).padStart(2, '0');
        const dd = String(now.getDate()).padStart(2, '0');

        const HH = String(now.getHours()).padStart(2, '0');
        const MM = String(now.getMinutes()).padStart(2, '0');
        const SS = String(now.getSeconds()).padStart(2, '0');

        return `${yyyy}.${mm}.${dd}. ${HH}:${MM}:${SS}`;
    }

}

 

3. Set origin

 

As mentioned earlier, the Target origin parameter of the iFrame component is an important factor from a security perspective. If the origin of the sender and the receiver is different, the message delivery will fail.

 

function handler({api, event, helpers, imports}) {
    api.setState("pageOrigin", helpers.getBaseUrl);
}

 

The script above runs when the page is loaded. It is enough to get this value once, and it can then be reused for each request.

 

AttilaVarga_3-1768834220241.png

 

Let’s take a high-level overview of the configuration.

AttilaVarga_4-1768834388316.png

 

The following animations show how the chat feature works in both Service Portal and Workspace.

 

Screen Recording 2026-01-26 at 11.00.27.gif

Using iFrame and Parent communication on Service Portal

Screen Recording 2026-01-26 at 11.01.48.gif

Using iFrame and Parent communication on Workspace

 

Diagrams with Draw.io (diagrams.net)

 

Now let’s move on to the actual solution: the integration of Draw.io.

There is a lot of useful material available on this topic, but I would like to highlight the following two sources: LINK, LINK

 

As it was shown in the previous section, the UI Builder iFrame component supports communication, which made it possible to embed and use Draw.io.

Let’s take a look at how this interaction works.

 

AttilaVarga_0-1769423604867.png

When Draw.io is loaded into the iFrame using a properly constructed URL (for example:https://embed.diagrams.net?param1=...&param2=...), the application sends a message to the parent window (init) as soon as it is ready for use.

The second step is to decide whether to start with a blank canvas or load an existing diagram.

The loading feature is quite limited, as only the Draw.io data format (mxfile) can be used. Unfortunately, the import model (like Mermaid diagram) option is not supported.

Diagrams created in Draw.io can be saved and later reloaded, and there is also an option to export them.

 

During development, we used all the operations which can be seen above.

In every communication, JSON data is exchanged between the two components. It is important to mention that when the UI Builder sends information to the iFrame component, it always converts the package into a JSON object. However, Draw.io expects a text message and parses it as the first step. This causes a type mismatch that needed to be handled.

We used a clever workaround: we applied the JSON.stringify() function twice to the JSON object. The iFrame’s built-in logic performed the first parsing, and the Draw.io receiver module performed the second one. After this, the embedded component started working correctly.

 

We wanted to keep the use of the Draw.io API at a low level, so we created a wrapper solution. From the UI Builder point of view, this appears as a single UI component. The component communicates with its parent through different events, which enables a much simpler and cleaner interaction.

 

The main behavior of our Draw.io component can be configured using component-based properties. These properties can be seen on the image below:

 

AttilaVarga_0-1769426202271.png

 

After our component was added to a UI Builder page and configured, it started working, as shown in the image below:

 

AttilaVarga_1-1769426257094.png

 

Working solution

From this point onwards, the communication between the parent window and our Draw.io component was already working, as well as the connection between the component and the embedded Draw.io.
The video below demonstrates how it works with a simple data opening process.

 

draw.io.video.gif

 

 

Closing thoughts

It is definitely important to emphasize that we learned a lot from this project. On one hand, we explored the possibilities of embedding Draw.io directly into a page. On the other hand, we tried and learned one of the key new features of ServiceNow Zurich release: the ability to create and develop custom components in UI Builder.

 

We managed to build a working solution that can significantly speed up tasks related to documentation.

 

Of course, we faced many challenges along the way, and solving them sometimes required a small effort and sometimes a lot of determination.

 

I hope this article helps to understand how ServiceNow supports communication with pages opened in an iFrame, and it also shows the wide range of use cases the platform offers.