What It Is
Jelly is ServiceNow's XML-based scripting language built on Apache Jelly that generates dynamic HTML content server-side through tag libraries and expression evaluation. It solves the problem of creating reusable UI components and email templates that need to access ServiceNow data and business logic during rendering, before the content reaches the browser. Unlike client-side JavaScript that runs after page load, Jelly executes on the ServiceNow application server during the page generation process, allowing direct access to GlideRecord queries, system properties, and user session data without additional AJAX calls.
Architecturally, Jelly sits in the presentation layer of ServiceNow's MVC architecture, residing in the System UI application as UI macros (sys_ui_macro) and in email notification templates (sysevent_email_template). The Jelly processor integrates with ServiceNow's rendering engine, executing before HTML generation and after business rule processing. It operates within the same security context as the requesting user, inheriting their ACL permissions and role-based access controls when querying data or calling server-side APIs.
The execution environment provides access to ServiceNow's server-side objects including GlideRecord, GlideSystem, and GlideUser through built-in variables like ${gs} and ${gs.getUser()}. Jelly scripts can instantiate new GlideRecord objects, execute database queries, call Script Includes, and access system properties without the security restrictions that limit client-side code. This server-side execution model makes it particularly powerful for generating dynamic content that depends on complex business logic or sensitive data that shouldn't be exposed to the client.
You cannot function without Jelly when building reusable UI components that need server-side data processing, creating email templates that require complex formatting or data aggregation, or maintaining legacy ServiceNow implementations built before Angular adoption. Custom portals, service catalog variable layouts, and complex notification templates frequently depend on Jelly for functionality that client-side JavaScript cannot provide due to security constraints or timing requirements. Email notifications particularly rely on Jelly because they execute in a server-side context where client-side JavaScript is meaningless, and the template must have fully rendered HTML content before transmission.
Platform administrators typically manage Jelly UI macros for system-wide components and email templates, while developers create application-specific macros and customize existing ones. Both roles need to understand Jelly syntax and ServiceNow's tag library extensions, though developers usually handle the more complex scripting scenarios involving database queries and business logic integration. System administrators often encounter Jelly when troubleshooting email notifications, customizing portal layouts, or maintaining legacy UI elements that haven't been migrated to newer technologies.
Starting with the Orlando release, ServiceNow began deprecating Jelly in favor of Angular components and client-side JavaScript frameworks, though existing Jelly implementations continue to function without modification. Vancouver and later releases include warnings in the developer console about Jelly usage and provide migration paths for common use cases. However, email templates and certain legacy UI elements still require Jelly, and complete removal isn't scheduled, making Jelly knowledge essential for maintaining existing ServiceNow implementations and understanding how legacy components function.
Where to Find and Configure It
Navigate to System UI > UI Macros to access the primary configuration interface where you create, edit, and manage Jelly-based UI components. This table (sys_ui_macro) contains all reusable UI macros including both Jelly and Angular implementations, with the XML field containing your Jelly code. Access email notification templates at System Notification > Email > Notification Templates where the Message HTML and Message Text fields support Jelly syntax for dynamic content generation.
In ServiceNow Studio, open any application scope and navigate to User Interface > UI Macros to create scoped UI macros with Jelly code, though App Engine Studio doesn't provide direct Jelly editing capabilities since it focuses on modern development approaches. View existing Jelly implementations in action by examining form layouts at System UI > Form Design or by checking the sys_ui_element table for form elements that reference UI macros. Portal pages using Jelly can be found under Service Portal > Portals and Service Portal > Pages where legacy implementations may still use Jelly-based widgets.
Scoped applications can contain Jelly UI macros that are isolated from global scope, accessible through the same sys_ui_macro table but filtered by the sys_scope field. Global UI macros are available system-wide and can be called from any scope, while scoped macros are only accessible within their application context unless explicitly shared. Email templates follow similar scoping rules, with global templates available across all applications and scoped templates restricted to their containing application's notification events.
How It Works Step by Step
When ServiceNow encounters a Jelly macro reference, the system loads the XML content from the sys_ui_macro table and passes it to the Apache Jelly processor for compilation and execution. The processor parses the XML structure, identifying Jelly tags, expressions, and static HTML content, then builds an execution tree that resolves variables and evaluates expressions within the current server-side context. All database queries, Script Include calls, and system property lookups execute with the same security permissions as the requesting user, ensuring proper access control enforcement during content generation.
The execution environment provides built-in variables including ${gs} for GlideSystem access, ${current} for the current record context, and parameters passed from the calling form or notification. Jelly processes control structures like loops and conditionals server-side, generating the final HTML output before sending anything to the browser. This server-side rendering approach means the client receives fully processed HTML without any Jelly syntax, making the dynamic content generation transparent to end users while enabling complex business logic integration.
Caching behavior varies by implementation context, with UI macros typically cached until the next system restart or cache flush, while email templates are processed fresh for each notification to ensure current data accuracy. The system maintains a parsed version of frequently used macros in memory to improve performance, but changes to the underlying XML content require cache invalidation to take effect. Error handling occurs server-side during execution, with Jelly syntax errors appearing in system logs and potentially causing blank output or error messages in the generated content.
Enjoying this? Get one deep-dive per week.
Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.
The Execution Order
- ServiceNow encounters a macro reference (like
${macro_name}) during form rendering or email template processing - System queries the
sys_ui_macrotable to retrieve the XML content and check if cached version exists - Jelly processor initializes execution context with current user session, record context, and passed parameters
- XML parsing occurs, identifying Jelly tags (
<j:forEach>,<j:if>), expressions (${variable}), and static content - Variables and expressions evaluate in order, executing GlideRecord queries, Script Include calls, and system property lookups
- Control structures process (loops iterate, conditions evaluate, nested macros execute recursively)
- Final HTML output generates and replaces the original macro reference in the parent document
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:g2="null">
<j:set var="jvar_incident_count" value="${gs.getProperty('incident.threshold', '10')}"/>
<div class="incident-summary">
<h3>Recent Incidents for ${current.caller_id.getDisplayValue()}</h3>
<j:set var="jvar_incidents" value="${current.caller_id.incident_caller_id.getAggregate('COUNT')}"/>
<j:if test="${jvar_incidents > jvar_incident_count}">
<div class="alert alert-warning">
<strong>High Activity:</strong> ${jvar_incidents} incidents in last 30 days
</div>
</j:if>
<j:forEach var="jvar_incident" items="${current.caller_id.incident_caller_id}" end="5">
<div class="incident-item">
<a href="incident.do?sys_id=${jvar_incident.sys_id}">${jvar_incident.number}</a>
- ${jvar_incident.short_description} (${jvar_incident.state.getDisplayValue()})
</div>
</j:forEach>
</div>
</j:jelly>Real-World Scenarios
Dynamic Incident Assignment History in Email Notifications
Your organization requires detailed assignment history in incident escalation emails to help managers understand how long each support group held the ticket. Standard notification templates only show current assignment information, but stakeholders need complete audit trail data with timestamps and group names to make informed escalation decisions.
<div class="assignment-history">
<h4>Assignment History</h4>
<j:set var="jvar_assignment_gr" value="${current.sys_class_name}"/>
<j:set var="jvar_audit_gr" value="new GlideRecord('sys_audit')"/>
<j:invoke on="${jvar_audit_gr}" method="addQuery">
<j:arg type="java.lang.String" value="tablename"/>
<j:arg type="java.lang.String" value="incident"/>
</j:invoke>
<j:invoke on="${jvar_audit_gr}" method="addQuery">
<j:arg type="java.lang.String" value="documentkey"/>
<j:arg type="java.lang.String" value="${current.sys_id}"/>
</j:invoke>
<j:invoke on="${jvar_audit_gr}" method="addQuery">
<j:arg type="java.lang.String" value="fieldname"/>
<j:arg type="java.lang.String" value="assignment_group"/>
</j:invoke>
<j:invoke on="${jvar_audit_gr}" method="orderBy">
<j:arg type="java.lang.String" value="sys_created_on"/>
</j:invoke>
<j:invoke on="${jvar_audit_gr}" method="query"/>
<table border="1" style="border-collapse: collapse; width: 100%;">
<tr><th>Date</th><th>Assignment Group</th><th>Changed By</th></tr>
<j:forEach var="jvar_audit" items="${jvar_audit_gr}">
<tr>
<td>${jvar_audit.sys_created_on.getDisplayValue()}</td>
<td>${jvar_audit.newvalue.getDisplayValue()}</td>
<td>${jvar_audit.sys_created_by.getDisplayValue()}</td>
</tr>
</j:forEach>
</table>
</div>Add this Jelly code to your incident notification template's Message HTML field and ensure the notification has Send HTML enabled. Watch for performance impact on high-volume notifications since this queries the audit table for every email, and consider adding date range filters if incident histories are extensive. The sys_audit table access depends on your audit configuration and retention policies.
Custom Service Catalog Item Layout with Dynamic Options
Your service catalog needs a hardware request form where available laptop models depend on the requestor's department and budget approval level. Static choice lists can't handle this dynamic relationship, and client-side JavaScript runs too late in the form rendering process to modify the initial display before users see it.
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:g2="null">
<j:set var="jvar_user_dept" value="${gs.getUser().getDepartmentID()}"/>
<j:set var="jvar_user_groups" value="${gs.getUser().getMyGroups()}"/>
<div class="hardware-options">
<label for="laptop_model">Available Laptop Models:</label>
<select name="laptop_model" id="laptop_model">
<j:set var="jvar_laptop_gr" value="new GlideRecord('cmdb_model')"/>
<j:invoke on="${jvar_laptop_gr}" method="addQuery">
<j:arg type="java.lang.String" value="cmdb_model_category.name"/>
<j:arg type="java.lang.String" value="Laptop"/>
</j:invoke>
<j:if test="${jvar_user_groups.indexOf('executive_staff') == -1}">
<j:invoke on="${jvar_laptop_gr}" method="addQuery">
<j:arg type="java.lang.String" value="u_budget_tier"/>
<j:arg type="java.lang.String" value="!=">
<j:arg type="java.lang.String" value="premium"/>
</j:invoke>
</j:if>
<j:invoke on="${jvar_laptop_gr}" method="query"/>
<j:forEach var="jvar_model" items="${jvar_laptop_gr}">
<option value="${jvar_model.sys_id}">
${jvar_model.display_name} - $${jvar_model.u_cost}
</option>
</j:forEach>
</select>
</div>
</j:jelly>Create this as a UI macro and reference it in your catalog item's variable layout or use it directly in a macro variable type. The hardware model lookup assumes you have custom fields like u_budget_tier and u_cost on the cmdb_model table. Be aware that this approach generates static HTML at initial page load, so if you need dynamic updates based on user selections, you'll need additional client-side JavaScript to refresh the content.
Jelly executes with the current user's permissions, so users without read access to queried tables will see empty results or errors.
Custom Knowledge Base Article Formatting with Related Content
Your knowledge management process requires articles to display related incidents and problem records automatically, formatted in a specific corporate template that includes metrics like resolution time and affected user counts. The standard knowledge base display doesn't provide this cross-referencing capability, and you need server-side processing to aggregate data from multiple tables.
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:g2="null">
<div class="related-content-section">
<h3>Related Incidents and Problems</h3>
<j:set var="jvar_kb_keywords" value="${current.u_keywords}"/>
<j:if test="${!empty(jvar_kb_keywords)}">
<div class="incidents-section">
<h4>Recent Incidents (Last 90 Days)</h4>
<j:set var="jvar_incident_gr" value="new GlideRecord('incident')"/>
<j:invoke on="${jvar_incident_gr}" method="addEncodedQuery">
<j:arg type="java.lang.String" value="short_descriptionLIKE${jvar_kb_keywords}^ORdescriptionLIKE${jvar_kb_keywords}^sys_created_on>=javascript:gs.daysAgoStart(90)"/>
</j:invoke>
<j:invoke on="${jvar_incident_gr}" method="orderByDesc">
<j:arg type="java.lang.String" value="sys_created_on"/>
</j:invoke>
<j:invoke on="${jvar_incident_gr}" method="setLimit">
<j:arg type="java.lang.Integer" value="10"/>
</j:invoke>
<j:invoke on="${jvar_incident_gr}" method="query"/>
<table class="related-incidents">
<tr><th>Number</th><th>Description</th><th>State</th><th>Created</th></tr>
<j:forEach var="jvar_incident" items="${jvar_incident_gr}">
<tr>
<td><a href="/incident.do?sys_id=${jvar_incident.sys_id}">${jvar_incident.number}</a></td>
<td>${jvar_incident.short_description}</td>
<td>${jvar_incident.state.getDisplayValue()}</td>
<td>${jvar_incident.sys_created_on.getDisplayValue()}</td>
</tr>
</j:forEach>
</table>
</div>
</j:if>
</div>
</j:jelly>Implement this macro in knowledge base article templates by adding it to the kb_knowledge form layout or creating a dedicated UI macro called from knowledge templates. This approach requires a custom field like u_keywords on knowledge articles to drive the search logic. Watch for performance issues if your keyword searches are too broad or your incident table is large, and consider implementing caching for frequently accessed articles. The encoded query syntax allows complex filtering but can be difficult to debug if the logic becomes too complex.
The Classic Mistake
Embedding server-side GlideRecord queries directly in Jelly without proper null checking or error handling.
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<g:evaluate var="jvar_incident_gr">
var gr = new GlideRecord('incident');
gr.addQuery('assigned_to', gs.getUserID());
gr.query();
gr.next();
gr.number + ' - ' + gr.short_description;
</g:evaluate>
<div class="incident-summary">
<h3>Your Current Incident: ${jvar_incident_gr}</h3>
<g:evaluate var="jvar_priority">
var gr2 = new GlideRecord('incident');
gr2.get(jvar_incident_gr.sys_id);
gr2.priority.getDisplayValue();
</g:evaluate>
<p>Priority: ${jvar_priority}</p>
<p>State: ${jvar_incident_gr.state.getDisplayValue()}</p>
</div>
</j:jelly>This fails catastrophically when the user has no assigned incidents, causing the entire UI macro to render blank or throw JavaScript errors in the browser. ServiceNow executes the g:evaluate server-side, but gr.next() returns false with no records, making subsequent field access undefined. The mistake is non-obvious because it works fine during testing when developers have test incidents assigned, but breaks for real users with empty result sets.
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<g:evaluate var="jvar_has_incident">
var gr = new GlideRecord('incident');
gr.addQuery('assigned_to', gs.getUserID());
gr.addQuery('state', 'IN', '1,2,3');
gr.query();
if (gr.next()) {
jvar_incident_number = gr.getDisplayValue('number');
jvar_incident_desc = gr.getDisplayValue('short_description');
jvar_incident_priority = gr.priority.getDisplayValue();
jvar_incident_state = gr.state.getDisplayValue();
true;
} else {
false;
}
</g:evaluate>
<j:choose>
<j:when test="${jvar_has_incident}">
<div class="incident-summary">
<h3>Your Current Incident: ${jvar_incident_number} - ${jvar_incident_desc}</h3>
<p>Priority: ${jvar_incident_priority}</p>
<p>State: ${jvar_incident_state}</p>
</div>
</j:when>
<j:otherwise>
<div class="no-incidents">
<p>No active incidents assigned to you.</p>
</div>
</j:otherwise>
</j:choose>
</j:jelly>Always wrap GlideRecord operations in conditional logic and use separate jvar variables for each field value - never chain property access on potentially null objects in Jelly expressions.
When to Use This vs Alternatives
Jelly is the right choice only for customizing legacy email notification layouts and maintaining existing UI macros that can't be easily converted to Service Portal widgets. Modern ServiceNow development should avoid Jelly entirely in favor of Angular components, UI Pages with client scripts, or Service Portal widgets.
Use Jelly When
You're modifying notification email templates that require server-side data processing before sending, or you're maintaining existing UI macros in Classic UI that would require significant rework to convert to modern alternatives. Jelly excels at server-side template rendering where the final HTML needs to be generated on the server before delivery, particularly in email contexts where client-side JavaScript isn't available.
Use Angular/Service Portal Instead
For any new user-facing interface development, interactive dashboards, or dynamic content that needs real-time updates, Service Portal widgets with Angular provide better performance, maintainability, and user experience. Modern widgets handle responsive design, accessibility, and mobile compatibility automatically, while Jelly requires manual handling of these concerns.
Hybrid Approach
You might need both when migrating legacy systems gradually - maintain existing Jelly-based email notifications while building new Service Portal interfaces for the same business processes. This approach allows you to modernize user interfaces without disrupting critical email workflows, but requires careful coordination to ensure data consistency between both presentation layers.
Platform Interactions & Side Effects
- Jelly scripts execute with elevated privileges during email generation, bypassing normal ACL restrictions and potentially exposing sensitive data in notification content
- UI macros using Jelly don't respect user session timezone settings - all date/time formatting uses server timezone unless explicitly converted with
gs.getUserDisplayValue() - Database queries in
g:evaluateblocks aren't logged in thesyslogtable, making performance troubleshooting difficult - Update Set captures for Jelly-based UI macros include the entire macro definition, but changes to referenced Script Includes or Business Rules require separate update sets
- Jelly rendering happens server-side before page load, so dynamic content changes require full page refreshes - no AJAX updates possible
- Email notifications with complex Jelly logic can timeout during batch sending, causing the
syseventrecord to remain inreadystate indefinitely - Jelly macros can trigger additional Business Rules when using
GlideRecord.update()withing:evaluateblocks, creating unexpected workflow cascades - Memory usage scales poorly with complex Jelly templates - each UI macro instance loads the full Apache Jelly parser into the application server heap
- Jelly script errors don't appear in normal JavaScript error logs - they're written to the
system logwith sourcejelly - Domain separation doesn't automatically apply to Jelly queries - you must explicitly add domain conditions using
gs.getDefaultDomain()in multi-domain instances
Debugging and Troubleshooting
The most common failure symptoms include blank UI macro sections rendering as empty divs, email notifications showing raw variable names like ${jvar_undefined} instead of data, or complete page rendering failures with generic "An error has occurred" messages. Users typically see missing content sections or malformed HTML, while administrators might notice emails not sending or UI pages loading partially. The challenge is that Jelly fails silently in many cases, continuing to render around problematic sections rather than throwing obvious errors.
Check System Logs > All filtered by source jelly for parsing errors and script exceptions. Enable debug logging by setting glide.jelly.debug to true in System Properties, which outputs detailed variable resolution and template processing information. For email-related issues, examine the sysevent table for stuck notification events and the sys_email table for generated email content to see exactly what the Jelly template produced.
Look for error messages containing "JellyException", "NullPointerException in jelly script", or "Unable to resolve variable" in the logs. Email notification failures often show "Template processing failed" with the specific line number where the Jelly parsing broke. UI macro errors typically appear as "Error rendering macro" followed by the macro name and the problematic Jelly construct that couldn't be processed.
Diagnostic Checklist:
- Validate XML syntax by copying the Jelly template into an XML validator - malformed tags cause silent failures
- Test all
g:evaluatescripts in isolation using Scripts - Background to verify the server-side logic works correctly - Add debug output by inserting
gs.log('Debug: ' + variable_name)statements withing:evaluateblocks to trace execution flow - Check user permissions by impersonating the affected user - ACL issues manifest differently in Jelly contexts
- Verify namespace declarations at the top of the Jelly template match the tags being used - missing xmlns attributes cause parsing failures
- Test with minimal data sets first - complex queries with large result sets can timeout without clear error messages
- Clear the cache by restarting the application server or using
cache.do- Jelly templates are aggressively cached and changes might not appear immediately
Quick Reference
- Jelly variable scope is global within a template - variables defined in nested
j:ifblocks remain accessible outside those blocks - Maximum template size is 65,535 characters in the
sys_ui_macro.xmlfield - larger templates get truncated silently - Email templates process Jelly during the notification event execution, not when the email is actually sent - timing matters for dynamic content
- The
trim="false"attribute is critical for email formatting - without it, whitespace gets collapsed and breaks HTML layout - Jelly expressions
${variable}are evaluated left-to-right with no operator precedence - use parentheses for complex expressions - UI macros can't access URL parameters directly - they must be passed explicitly through macro arguments when included in UI Pages
- Nested
g:evaluateblocks create separate script contexts - variables from outer blocks aren't automatically available in inner blocks - Jelly templates execute with the same security context as the user triggering the action - system-level operations require explicit privilege elevation
- Performance degrades exponentially with nested loops - avoid
j:forEachwithinj:forEachand limit outer loops to 50 iterations maximum - Special characters in Jelly variables must be HTML-encoded manually - there's no automatic escaping like in modern template engines