Structr
Expert Topics
Migration Guide
This chapter covers breaking changes and migration steps when upgrading between major Structr versions.
Important: Always create a full backup before upgrading Structr.
Migrating to Structr 7.x
Version 7 changes the signatures of the outbound HTTP functions. All optional arguments moved into a
single trailing options object, so a call no longer has to pass positional nulls to reach the last one,
and an option that is not supported by a function is rejected instead of being silently ignored.
Outbound HTTP Function Signatures
| Function | Signature |
|---|---|
$.GET | url [, contentType [, options ]] |
$.HEAD | url [, options ] |
$.POST | url, body [, contentType [, options ]] |
$.PUT | url, body [, contentType [, options ]] |
$.PATCH | url, body [, contentType [, options ]] |
$.DELETE | url [, body [, contentType [, options ]]] |
$.FETCH | url, method [, body [, contentType [, options ]]] |
$.POST_multi_part | url, partsMap [, options ] |
// Old (6.x): charset, then credentials, as positional arguments
$.POST(url, body, 'application/json', 'UTF-8', 'user', 'secret');
// New (7.x): the charset belongs to the content type, everything else is an option
$.POST(url, body, 'application/json; charset=UTF-8', { username: 'user', password: 'secret' });
username, password, preemptive, headers, timeout, redirects and validateCertificates are
accepted by every function. selector, binaryResponse and parseResponse are accepted
only where they mean something; passing one elsewhere is an error naming the function and the key.
Most calls can be rewritten automatically. See application.migration.mode and the migrate maintenance
command, which report every call that still uses the old form and can rewrite the unambiguous ones.
Binary Responses Are Streams
$.POST previously returned the response body as a byte array when the content type was
application/octet-stream. Streaming the response is now requested by the binaryResponse option, the
same key $.GET uses, and the result is an InputStream for both. The stream removes the 2 GB limit
that the array imposed.
// Old (6.x): a byte array
const bytes = $.POST(url, body, 'application/octet-stream').body;
// New (7.x): a stream, which can be passed straight to setContent()
const stream = $.POST(url, body, 'application/octet-stream', { binaryResponse: true }).body;
$.get_or_create('File', { name: 'result.bin' }).setContent(stream);
This change cannot be detected automatically. The call text is unchanged, so the migration report
will not flag it: any code that indexes into the result, measures its length or stores it as a byte array
has to be adjusted by hand.
Failed Requests No Longer Throw
A request that reached the server has always returned its status. A request that did NOT reach it - an
unreachable host, a refused connection, a TLS failure, a timeout - used to throw a FrameworkException
with status 422. It now returns a response with status: 0 and an error field describing the failure.
// Old (6.x): only a try/catch could survive an unreachable service, and StructrScript could not
try { $.GET(url); } catch (e) { /* ... */ }
// New (7.x): check the status, which StructrScript can do as well
let response = $.GET(url);
if (response.status === 0) { $.log('unreachable: ' + response.error); }
Two related corrections come with it. An error response WITHOUT a body, such as a bodyless 404 or a
204, used to be reported as a thrown 422 because the empty body was read unchecked; it now returns the
status the server sent. And a rejected URL used to be re-wrapped as that same 422; its own status now
survives.
Calls that cannot be made at all still throw: a malformed URL, a scheme other than http or https, a URL
without a host, an internal network address, or a URL outside the outgoing whitelist.
Code that relies on a catch to detect an unreachable service will no longer enter it. Check
status === 0 instead.
Binary Request Bodies
A File passed as the request body is now sent as its content rather than as its string representation,
so an upload no longer needs base64 or a multipart envelope. See the Filesystem chapter.
PDF Generation Without wkhtmltopdf
The pdf() function and the PdfServlet no longer call the external wkhtmltopdf binary. The document
is now produced inside the JVM, so nothing has to be installed on the server and PDF generation works
from a cron job or a doPrivileged context as well as from a request.
Three things change for existing applications.
pdf() returns a File, not a string. The old return value was the document’s bytes carried in an
ISO-8859-1 string, which every caller then wrote into a file.
// Old (6.x)
${ set_content(create('File', 'name', 'report.pdf'), pdf('report'), 'ISO-8859-1') }
// New (7.x)
${ pdf('report', 'report.pdf') }
The wkhtmltopdf parameters are gone. The second parameter is now the name of the generated file.
Passing an argument string raises an error rather than being ignored, because a silently dropped
--header-html produces a document that looks right and is missing its header.
A detail object still comes from the path, but parameters are now an argument. The old function
built a URL, so everything travelled in one string. The page path still carries the object the page
renders, which it reads as current, while request parameters are passed as an object instead of a
query string. A query string in the path is refused rather than ignored.
// Old (6.x)
${ pdf(concat('invoice/', order.id, '?lang=de')) }
// New (7.x)
${ pdf(concat('invoice/', order.id), 'invoice.pdf', { lang: 'de' }) }
The page reads those as ${request.lang}, and it sees exactly the parameters passed and no others, so
the same call produces the same document from a page, a cron job or doPrivileged.
Headers, footers and page numbers move into the print stylesheet. They used to be separate Structr
pages fetched over HTTP. They are now page level CSS, and no second page is involved:
@page {
size: A4;
margin: 25mm 18mm;
@top-left { content: element(docheader); }
@bottom-right { content: "Page " counter(page) " of " counter(pages); }
}
#docheader { position: running(docheader); }
The renderer implements CSS 2.1 plus paged media. Flexbox, grid, custom properties and JavaScript have
no effect on paper, so a page whose screen layout relies on them needs a print stylesheet. Declarations
the renderer cannot use are written to the server log rather than dropped in silence, so the log tells
you what a document lost.
Images, stylesheets and fonts are read from the Structr filesystem by path, under the permissions of the
user the page is rendered as. External URLs are not fetched unless pdf.resources.external.allowed is
enabled.
Migrating to Structr 6.x
Version 6 introduces several breaking changes that require manual migration from 5.x.
Global Schema Methods
Global schema methods have been simplified. The globalSchemaMethods namespace no longer exists – functions can now be called directly from the root context.
StructrScript / JavaScript:
// Old (5.x)
$.globalSchemaMethods.foo()
// New (6.x)
$.foo()
REST API:
# Old (5.x)
/structr/rest/maintenance/globalSchemaMethods/foo
# New (6.x)
/structr/rest/foo
Action required: Search your codebase for
/maintenance/globalSchemaMethodsand$.globalSchemaMethodsand update all occurrences.
REST API Query Parameter Change
The _loose parameter has been renamed to _inexact.
# Old (5.x)
/structr/rest/foo?_loose=1
# New (6.x)
/structr/rest/foo?_inexact=1
REST API Response Structure
The response body from $.GET and $.POST requests is now accessible via the body property.
// Old (5.x)
JSON.parse($.GET(url))
// New (6.x)
JSON.parse($.GET(url).body)
Schema Inheritance
The extendsClass property on schema nodes has been replaced with inheritedTraits.
// Old (5.x)
eq('Location', get(first(find('SchemaNode', 'name', request.type)), 'extendsClass').name)
// New (6.x)
contains(first(find('SchemaNode', 'name', request.type)).inheritedTraits, 'Location')
JavaScript Function Return Behavior
JavaScript functions now return their result directly by default.
Option 1: Restore old behavior globally:
application.scripting.js.wrapinmainfunction = true
Option 2: Remove unnecessary return statements from functions.
JavaScript Strict Mode
Identifiers must be declared before use. Assigning to undeclared variables throws a ReferenceError.
// ❌ Not allowed
foo = 1;
for (foo of array) {}
// ✅ Correct
let foo = 1;
for (let foo of array) {}
Custom Indices
Custom indices are dropped during the upgrade to 6.0.
Action required: Recreate all custom indices manually after upgrading.
Upload Servlet Changes
| Aspect | 5.x Behavior | 6.x Behavior |
|---|---|---|
| Default upload folder | Root or configurable | /._structr_uploads |
| Empty folder setting | Allowed | Enforced non-empty |
uploadFolderPath | Unrestricted | Authenticated users only |
Repeaters: No REST Queries
REST queries are no longer allowed for repeaters. Migrate them to function queries or flows.
Migration Checklist for 6.x
- [ ] Replace
$.globalSchemaMethods.xyz()with$.xyz() - [ ] Update REST URLs: remove
/maintenance/globalSchemaMethods/ - [ ] Replace
_loosewith_inexact - [ ] Update
$.GET/$.POSTcalls to use.body - [ ] Replace
extendsClasswithinheritedTraits - [ ] Review JavaScript functions for return statement compatibility
- [ ] Declare all JavaScript variables properly
- [ ] Recreate custom indices after upgrade
- [ ] Review upload handling code
- [ ] Migrate repeater REST queries to function queries
Migrating to Structr 4.x
All versions starting with the 4.0 release include breaking changes which require migration of applications built with Structr versions prior to 4.0 (1.x, 2.x and 3.x).
GraalVM Migration
With version 4.0, the required Java Runtime changed from standard JVMs (OpenJDK, Oracle JDK) to GraalVM. GraalVM brings full ECMAScript support, better performance, and polyglot scripting capabilities.
Installing GraalVM
Each Structr version supports the stable GraalVM version current at the time of release. The following example shows installation on Linux:
wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.1.0/graalvm-ce-java11-linux-amd64-22.1.0.tar.gz
tar xvzf graalvm-ce-java11-linux-amd64-22.1.0.tar.gz
sudo mv graalvm-ce-java11-22.1.0 /usr/lib/jvm
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/graalvm-ce-java11-22.1.0/bin/java 2210
sudo update-alternatives --auto java
Migration of Script Expressions
Predicates in find() and search()
All predicates in find() and search() expressions need the $.predicate prefix. The easiest way to migrate is to export the application using deployment export and search all files for these predicates:
$.and
$.or
$.not
$.equals
$.contains
$.empty
$.range
$.within_distance
$.sort
$.page
Examples:
// Old (3.x)
$.find('File', 'size', $.range(null, 100), $.page(1, 10));
// New (4.x+)
$.find('File', 'size', $.predicate.range(null, 100), $.predicate.page(1, 10));
// Old (3.x)
$.find('User', $.sort('createdDate'));
// New (4.x+)
$.find('User', $.predicate.sort('createdDate'));
Some predicates also exist as regular functions ($.sort(), $.empty()) or keywords ($.page). When used outside of find(), they don’t need changes:
// No change needed - sort() used outside find()
$.sort($.find('User'), 'createdDate');
Resource Access Permissions
Resource Permissions (formerly “Resource Access Grants”) have been made more flexible. Rights management now also applies to permission nodes themselves, requiring users to have read access to the permission object to use it.
Manual Migration
- Log in as admin
- Navigate to Security → Resource Permissions
- Enable “Show only used grants”
- Migrate permissions:
- If the permission has active flags for “Public Users”: set
visibleToPublicUsers = true - If the permission has active flags for “Authenticated Users”: set
visibleToAuthenticatedUsers = true - If both flags apply: split into two permissions with identical signatures
For many permissions, enable “Show visibility flags in Resource Permissions table” in Dashboard → UI Settings.
Semi-automatic Migration via Deployment
When importing a deployment export from a pre-4.0 version into 4.x+, Structr runs automatic migration using this heuristic:
- Public Users flags →
visibleToPublicUsers = true - Authenticated Users flags →
visibleToAuthenticatedUsers = true - If both flags are set, a warning is issued to split the grant (since
visibleToPublicUsers = truealso makes the object visible to authenticated users)
Scripting Considerations
Date Comparisons
Use the getTime() function when comparing dates to avoid issues with GraalVM ProxyDate entities:
{
return $.me.createdDate.getTime() <= $.now.getTime();
}
Conditional Chaining Limitation
Conditional chaining on ProxyObjects with function members can cause errors:
{
const obj = {
method1: () => "works"
};
// Works
obj.method1?.();
// Works, call doesn't get executed
obj.method2?.();
const proxyObject = $.retrieve('passedObject');
// Does NOT work - throws unsupported message exception
proxyObject.myMethod?.();
}
REST Request Parameters
Starting with 4.0, REST request parameters must be prefixed with underscore to prevent name collisions with property names:
# Old
/structr/rest/Project?page=1&pageSize=10&sort=name
# New
/structr/rest/Project?_page=1&_pageSize=10&_sort=name
Full list of affected parameters:
| Parameter | Parameter | Parameter |
|---|---|---|
page | pageSize | sort |
order | loose | locale |
latlon | location | state |
house | country | postalCode |
city | street | distance |
outputNestingDepth | debugLoggingEnabled | forceResultCount |
disableSoftLimit | parallelizeJsonOutput | batchSize |
Legacy mode can be enabled with application.legacy.requestparameters.enabled = true but is discouraged for new projects.
Neo4j Upgrade
Neo4j 4.x is recommended for Structr 4.x, though Neo4j 3.5 is still supported. If upgrading Neo4j, consult the Neo4j changelog.
Cypher Parameter Syntax
The old parameter syntax {param} was deprecated in Neo4j 3.0 and removed in Neo4j 4.0. Use $param instead. For compatibility, you can prefix queries with CYPHER 3.5.
Database Name Configuration
If migrating from Neo4j versions prior to 4, the default database may be named graph.db instead of neo4j. Configure the database name in structr.conf:
YOUR_DB_NAME.database.connection.url = bolt://localhost:7687
YOUR_DB_NAME.database.connection.name = YOUR_DB_NAME
YOUR_DB_NAME.database.connection.password = your_neo4j_password
YOUR_DB_NAME.database.connection.databasename = graph.db
YOUR_DB_NAME.database.driver = org.structr.bolt.BoltDatabaseService
Migration Checklist for 4.x
- [ ] Install GraalVM as Java runtime
- [ ] Add
$.predicateprefix to all find/search predicates - [ ] Update Resource Permissions with visibility flags
- [ ] Split Resource Permissions that have both public and authenticated flags
- [ ] Prefix REST parameters with underscore
- [ ] Update date comparisons to use
getTime() - [ ] Review code for conditional chaining on ProxyObjects
- [ ] Update Neo4j configuration if upgrading database
- [ ] Update Cypher parameter syntax if using Neo4j 4.x
Creating Widgets
This chapter covers how to write your own Widgets: static building blocks, page templates, data-driven components and render templates. It builds on the Widgets & Components chapter, which explains how Widgets and Shared Components are used, and on the template expressions described in the Dynamic Content chapter.
The widget system consists of three subsystems that are separated from each other by design. The component type system controls where a Widget can be inserted and how Widgets nest. The replacement mechanism controls what happens when you replace an element with a Widget or wrap it in one. Data-driven components render their content from a data source and a ComponentConfiguration, using a small set of rendering functions. A good way to learn all three is to import the default widget set and read the source of its Widgets alongside this chapter.
How a Widget Becomes Page Elements
A Widget is an object in the database with an HTML source code field. When you insert it, Structr parses the source and creates the corresponding elements in the page tree. In the simplest case the source is plain HTML, and the resulting elements are independent copies with no connection to the Widget. Inserting the same Widget twice creates two separate sets of elements.
The source can define Shared Components with <structr:shared-template> tags. Structr creates each Shared Component only once. When the Widget is inserted again, Structr finds the existing Shared Component by name and references it instead of creating a duplicate. This is the mechanism behind widget libraries, where every Widget produces the same Shared Components on every page.
The source can contain template variables in square brackets, such as [variableName]. Structr treats a square bracket expression as a variable only when a key with the same name exists in the Widget configuration. All other square bracket expressions are left unchanged. When a Widget has at least one recognized variable, the Insert Widget dialog asks for the values before insertion.
The source can also contain deployment annotations, the HTML comments that the application export writes in front of elements to preserve Structr attributes. Set processDeploymentInfo to true in the Widget configuration to have them interpreted on insertion. This is useful when you build the Widget source from an exported page.
The Widget Editor
The plus button in the Widgets flyout creates a new Widget and opens the editor. The editor has five tabs: Source, Configuration, Description, Options and Help.
Source
The Source tab holds the HTML source of the Widget, including Structr expressions. The easiest way to obtain it is to build the elements in a page and export their markup with the _edit=1 URL parameter, which renders the page with all expressions and Structr attributes intact instead of evaluating them:
- Build the Widget in a page called
myWidgetPage - Open
http://localhost:8082/myWidgetPage?_edit=1 - View and copy the source code of the page
- Paste it into the Source tab
Setting Structr Attributes in the Source
Widget source is plain HTML, but Structr elements have attributes that do not exist in HTML, such as componentType or contentType. To set them during insertion, use HTML attributes with the prefix data-structr-meta-. Take the Structr attribute name in camelCase, convert it to kebab-case and add the prefix. Structr strips the prefix on import, converts the name back to camelCase and sets the attribute on the element.
| HTML attribute | Structr attribute |
|---|---|
data-structr-meta-name | name |
data-structr-meta-component-type | componentType |
data-structr-meta-content-type | contentType |
data-structr-meta-dimensions | dimensions |
data-structr-meta-show-conditions | showConditions |
data-structr-meta-hide-conditions | hideConditions |
data-structr-meta-item-type | itemType |
data-structr-meta-repeater-type | repeaterType |
data-structr-meta-data-key | dataKey |
data-structr-meta-function-query | functionQuery |
data-structr-meta-triggered-actions | The event action mapping of the element, as a JSON object |
The mechanism works for any Structr attribute, not only the ones listed here.
Configuration
The Configuration tab defines the template variables of the Widget as a JSON object. Each key is a variable name that matches a square bracket expression in the source, and its value describes the form field that the Insert Widget dialog shows for it. The configuration must be valid JSON.
{
"configSwitch": {
"position": 2,
"default": "This is the default text"
},
"selectArray": {
"position": 3,
"type": "select",
"options": [
"choice_one",
"choice_two",
"choice_three"
],
"default": "choice_two"
},
"selectObject": {
"position": 1,
"type": "select",
"options": {
"choice_one": "First choice",
"choice_two": "Second choice",
"choice_three": "Third choice"
},
"default": "choice_two"
},
"processDeploymentInfo": true
}
Two kinds of top-level keys are reserved. processDeploymentInfo is the boolean described above. Keys that begin with an underscore are meta entries that are not shown as form fields. The process module uses _autoVisibilityMapping in this way to bind a Widget to a process step on insertion.
Each variable supports these attributes:
| Attribute | Applies to | Description |
|---|---|---|
title | all | The title displayed in the dialog. If omitted, the variable name is used. |
placeholder | input, textarea | The placeholder text displayed when the field is empty. If omitted, the title is used. |
default | all | The default value. For input and textarea it is prefilled, for select it is preselected. |
position | all | A number for sorting the fields. Variables without a position appear after those with a position, in natural key order. |
help | all | Help text displayed when hovering over the information icon. |
comment | page, boolean | Help text for the typed fields that share their rendering with the render template settings. |
type | all | The form field type. See the table below. Defaults to input. |
options | select | An array of strings or an object with value-label pairs. Arrays render as simple options. Objects use the key as value and the object value as displayed text. |
dynamicOptionsFunction | select | A function body that populates the options. The function receives a callback parameter that must be called with the options. If present, options is ignored. |
rows | textarea | The number of rows. Defaults to 5. |
The type attribute selects the form field:
| Type | Form field |
|---|---|
input | A text input. This is the default. |
textarea | A multi-line text input. |
select | A dropdown with the given options. |
boolean | A checkbox. |
page | A dropdown listing the pages of the application. |
schema-type | A dropdown listing the custom types of the application. |
schema-property | A dropdown listing the properties of the type selected in the schema-type field of the same dialog. |
schema-method | A dropdown listing the methods of the type selected in the schema-type field of the same dialog. |
datasource | The data source menu. Structr replaces the variable with the resolved data source name. See Authoring Data-Driven Components below. |
process | A dropdown listing the BPMN processes of the application. Requires the process module. |
user-task | A dropdown listing the user tasks of the process selected in the process field of the same dialog. Requires the process module. |
The types schema-property, schema-method and user-task read their options from a sibling field, so they only work together with a schema-type or process field in the same configuration. The Action Button of the default set combines schema-type and schema-method to let the user pick the method the button calls.
Description
The Description tab holds text that is shown at the top of the Insert Widget dialog and on the tile in the Create Page dialog for page templates. It can contain HTML and typically explains what the Widget does and what the configuration fields mean.
Options
The Options tab holds the settings that control where and how the Widget is offered:
| Setting | Description |
|---|---|
| Selectors | CSS selectors that decide under which elements the Widget is suggested. [type='container'] suggests the Widget inside any element whose component type is container. Several selectors are combined with commas. For render templates, the selectors hold the compatible data types instead. |
| Is Page Template | Shows the Widget in the Create Page dialog. |
| Is Render Template | Marks the Widget as a render template for fields of data-driven components. |
| Is Exclusive In Parent | Allows only one instance per parent element. Once an element created by the Widget exists under a parent, the Widget is no longer suggested there. |
| Short Description | A one-line description of the Widget. |
| Component Type | The component type that Structr assigns to the root element created by the Widget. |
| Dimensions | The dimensions value that Structr assigns to the root element created by the Widget. |
The flyout context menu entry “Advanced” opens the full properties dialog of the Widget, which additionally holds the paths of the thumbnail and the icon shown in the flyout and in the Create Page dialog.
The Component Type System
The component type system controls where Widgets can be inserted and how they nest. It uses two attributes on the page elements. componentType gives an element a role in the nesting hierarchy, and dimensions describes the shape of data the element is designed to display.
Component Types and Selectors
The values of componentType are defined freely by the author of a widget set. Structr does not impose a fixed set. The selectors of a Widget are standard CSS selectors that Structr matches against a synthetic element built from the prospective parent: its tag name, CSS classes, id and name, plus the component type of the parent as the attribute type. If the parent is a linked copy of a Shared Component, the component type of the Shared Component is used. [type='container'] therefore suggests a Widget inside every element whose component type is container, and table suggests it inside every table element.
The default widget set uses these component types:
| componentType | Description |
|---|---|
| canvas | The main content area of a page. Accepts the top-level components. |
| container | A structural container for layout and grouping. |
| form | A container with submit semantics. Accepts inputs directly. |
| content | A leaf element that displays content. Cannot have children. |
| input | A leaf element for user input. Cannot have children. |
| action | A leaf element that triggers an action, such as a button. |
| table, list, gallery | Data-driven components that render a collection. Inserted into a canvas. |
| table-cell | The cells of a data-driven table. Accept input, action and text widgets, so a table can be edited in place. |
There are two ways to assign a component type to the elements a Widget creates. The Component Type and Dimensions settings in the Options tab are applied to the root element on insertion, or to the Shared Component behind it. Alternatively, data-structr-meta-component-type on a <structr:shared-template> or any other element in the source sets the type for that element. The second way is needed when a Widget defines several elements with different roles, for example a card whose content area is a container while the card itself is not.
Dimensions
The dimensions attribute counts the axes along which the content of a component repeats:
| dimensions | Meaning | Content structure | Examples in the default set |
|---|---|---|---|
| 0 | A single object | Fixed layout with named areas, no repetition | Card, Edit Form |
| 1 | A list | A repeating sequence of identical areas | List, Gallery, Accordion |
| 2 | A table | Areas arranged in rows and columns | Table |
For data-driven components, the value also constrains the data source. A component with dimensions 0 expects a single object, dimensions 1 a collection, and dimensions 2 a collection with several properties per item. Structr checks this whenever the configuration of a component changes, see Data Source Compatibility below. Leaf elements and forms have no dimensions value, and a missing value means that the data source is not constrained.
Exclusive Widgets
A Widget with “Is Exclusive In Parent” is suggested only while no element created by it exists under the prospective parent. Structr recognizes such elements by name, so the root element of an exclusive Widget must carry the Widget’s name, either as the name of its Shared Component or via data-structr-meta-name. Use this for elements that make sense only once per container, such as a page heading or a pagination bar.
Replacing and Wrapping
The context menu offers two operations that exchange an existing element for a Widget. “Replace Element With” removes the selected element and inserts the Widget in its place. “Wrap Element In” inserts the Widget in place of the selected element and moves the element into one of the Widget’s content areas. For both, Structr suggests only Widgets whose selectors match the parent of the selected element, using the same matching as for insertion. For wrapping, the Widget must additionally contain an element whose component type matches the selectors of the Widget that created the selected element, so that the element has a valid place to go.
When an element is replaced, Structr transfers matching content areas and repeaters from the old element to the new Widget. Content areas are matched by itemType, repeaters by repeaterType. Both attributes are set freely by the widget author, matching is by string equality, and each value may occur only once in a Widget. The Card with heading of the default set marks its heading with data-structr-meta-item-type="label" and its body with data-structr-meta-item-type="content", so replacing one card with another keeps heading and body in place.
itemType and repeaterType belong exclusively to the replacement mechanism. They are distinct from componentType and from the ComponentConfiguration described below. Data-driven components are not subject to content transfer. When one data-driven component is replaced with another, only the data source binding is carried over, since there is no manually placed content to preserve.
Building a Widget Library
A widget library is a set of Widgets designed to work together through the component type system. Each Widget defines one or more Shared Components with componentType, dimensions and selectors that enforce the nesting rules. When a Widget from the library is inserted, Structr creates its Shared Components if they do not exist yet, or reuses them if they do. A library can contain static Widgets and data-driven components.
Defining Widgets
Use <structr:shared-template> tags for the Shared Component definitions and <structr:template> tags to reference them. Container Widgets call render(children) to mark the content area where child elements go. Leaf Widgets omit the call. The following source defines four Shared Components and assembles them into a page:
<!-- Shared Component definitions -->
<structr:shared-template name="Main Page Template"
data-structr-meta-content-type="text/html">
<html>
<head>
<title>${page.name}</title>
</head>
<body>
${render(children)}
</body>
</html>
</structr:shared-template>
<structr:shared-template name="Main Content"
data-structr-meta-component-type="canvas"
data-structr-meta-content-type="text/html">
<main>${render(children)}</main>
</structr:shared-template>
<structr:shared-template name="Panel"
data-structr-meta-component-type="container"
data-structr-meta-dimensions="0"
data-structr-meta-content-type="text/html">
<div class="panel panel-content">
${render(children)}
</div>
</structr:shared-template>
<structr:shared-template name="Paragraph"
data-structr-meta-component-type="content"
data-structr-meta-content-type="text/html">
<p>Text</p>
</structr:shared-template>
<!-- References: assemble the page structure -->
<structr:template src="Main Page Template">
<structr:template src="Main Content">
<structr:template src="Panel">
<structr:template src="Paragraph"></structr:template>
</structr:template>
</structr:template>
</structr:template>
The definitions create the Shared Components, the references assemble them: Main Page Template contains Main Content, which contains a Panel with a Paragraph. Inserting the Widget again reuses the existing Shared Components. With “Is Page Template” set, this Widget appears in the Create Page dialog, and every page created from it shares the same Main Page Template.
Multi-Area Widgets
Some container Widgets provide more than one content area. The Card with heading of the default set renders the card frame in its Shared Component and places two elements inside it through the referencing <structr:template>: a heading with data-structr-meta-item-type="label" and a container with data-structr-meta-item-type="content" and data-structr-meta-component-type="container". The itemType values name the areas for the replacement mechanism, and the component type of the content area decides which Widgets are suggested inside it. The Widget owns its internal structure and exposes the areas for the user to fill.
Organizing the Library
Use the treePath attribute to group Widgets into categories in the flyout and in the Suggested Widgets submenu. The path is slash-separated, must begin with a slash, and may contain spaces. Widgets without a path appear under “Uncategorized”. The default set uses these categories:
/Layout
/Components
/Component Parts
/Form Elements
/Text
/Actions
/Render Templates
/Page Templates
Render templates and page templates get categories of their own because they are not inserted directly.
Using the Library
Once the Widgets carry component types, dimensions and selectors, the library integrates into the page building workflow. A page created from the page template starts with a canvas that accepts containers and components. Right-clicking the canvas suggests exactly those, right-clicking inside a container suggests content, inputs and actions. The component type system guides the user through a valid page structure without knowledge of the rules.
Authoring Data-Driven Components
A Template element becomes a data-driven component when a ComponentConfiguration is attached to it. This happens when the Widget source contains a <structr:template> with a config attribute. The attribute holds the initial values of the configuration as a JSON-like object. The Table of the default set uses:
<structr:template src="Table" config="{ dataSource: '[dataSource]', reload: 'partial' }"></structr:template>
The keys correspond to the properties of the ComponentConfiguration: dataSource, displayMode (output or input), role (none, controller or subscriber), selectionChannel, reload (partial, page or others), labels, pageSize, columns and transform. The Edit Form presets displayMode: 'input', role: 'subscriber', labels: true so that the user only has to pick the channel.
The [dataSource] variable is filled from the Insert Widget dialog. The Widget declares it in its configuration with the type datasource:
{
"dataSource": {
"title": "Data Source",
"type": "datasource"
}
}
The dialog then shows the data source menu. When the user picks an existing type or channel, the variable is replaced with the data source name, for example node:Project or channel:current. When the user creates a new type, Structr creates the schema type and its properties, fills the field set of the component with the new attributes and, if requested, generates example data. When the user creates a script-based or query-based data source, Structr creates the empty node and binds the component to it by id.
Keywords in Component Templates
Inside the Shared Component or template of a data-driven component, these keywords are available:
component refers to the closest data-driven component above the current element and exposes its state and helper methods: pagination(), page(), pageCount, filterControls() and isEditable, which is true in input mode and serves as a show condition for a save button. From outside the component, getComponent('Name') returns the component with the given name.
dataSource refers to the data source of the component. dataSource.selectedValue is the record that a subscriber received via its channel and is only available when the data source is a channel. The Edit Form uses it as the id expression of its update action and in empty(dataSource.selectedValue) to hide the save button while nothing is selected. dataSource.currentValue is the record of the current iteration inside renderEach(). The Delete Button uses dataSource.currentValue.id in a table row. dataSource.dataType returns the name of the type the data source delivers.
adapter refers to the data adapter of the component. adapter.dataType returns the type name and is used by the default forms in their button labels.
Rendering Functions
Three functions render the configured fields. Each takes a tag specification as its first argument, a small CSS selector consisting of a tag name with optional classes and an id, such as th.sw-table-header. The rendered wrapper receives the col-span-N class for the width of the field, or col-span-6 when no width is set, so the six-column grid works inside every component.
renderEach(tags) iterates over the paginated, filtered and sorted collection of the data source and renders the configured fields for each record. The argument names two tags separated by a space, the first for each record and the second for each field. The Table uses renderEach('tr td.sw-table-cell'), the List uses renderEach('li.sw-list-item'), the Accordion uses renderEach('details summary'). Each record wrapper receives the attributes that make the record selectable when the component is a controller.
renderFields(tag [, slot]) renders the configured fields for a single record, applying each field’s render template in output mode or its edit template in input mode. The Edit Form calls renderFields('div') inside a grid grid-cols-6 container so that the field widths take effect.
renderLabels(tag [, slot]) renders the labels of the configured fields without values. The Table calls renderLabels('th.sw-table-header') in its header row. The rendered labels carry the attributes that make a column sortable. Clicking a label sorts by the field’s sort key and toggles the direction, and the active label receives the CSS class ascending or descending.
Slots
A slot is a named position in a component template. A render template can assign its fields to slots, and renderFields('div', 'media') then renders only the fields of the media slot. renderLabels() accepts the slot argument in the same way. Slots are rarely needed. Without a slot argument all configured fields are rendered in order, which works for tables, lists and forms. Slots become useful when a template has fixed structural positions, for example a card where image, title and action must appear in specific places regardless of field order.
Pagination and Filtering
The component method pagination() returns the HTML attributes that wire a button to the pagination state. Its first argument names the kind of button: prev, next, first and last lead to the neighbouring pages and to the first and last page, window takes an offset relative to the current page as second argument and produces the numbered buttons, and ellipsis takes low or high and marks the placeholder buttons that appear when the first or last page is far away. page() takes the same arguments and returns the page number the button leads to. pageCount returns the total number of pages. The pagination bar of the Table reads:
<div class="flex flex-row p-4 gap-4 justify-between">
<button ${component.pagination('prev')}>Prev</button>
<div class="flex gap-4">
<button ${component.pagination('first')}>1</button>
<button ${component.pagination('ellipsis', 'low')}>…</button>
<button ${component.pagination('window', -2)}>${component.page('window', -2)}</button>
<button ${component.pagination('window', -1)}>${component.page('window', -1)}</button>
<button ${component.pagination('window', 0)}>${component.page('window', 0)}</button>
<button ${component.pagination('window', 1)}>${component.page('window', 1)}</button>
<button ${component.pagination('window', 2)}>${component.page('window', 2)}</button>
<button ${component.pagination('ellipsis', 'high')}>…</button>
<button ${component.pagination('last')}>${component.pageCount}</button>
</div>
<button ${component.pagination('next')}>Next</button>
</div>
Buttons that lead outside the valid range receive the hidden attribute, buttons without a valid target are disabled, and the button for the current page carries data-current-page. The same markup is shipped as the standalone “Pagination” Widget in the Component Parts category, which can be inserted into any container inside a component. The number of numbered buttons is controlled by the paginationWindowSize property of the ComponentConfiguration, default 5, which has no field in the dialog and is set in the Advanced properties.
filterControls() returns the attributes that wire a text input to the filter state of the component:
<input type="text" placeholder="Filter.." ${component.filterControls()} />
Typing performs a contains-search across all fields with “Include in filter” enabled, resets the pagination to the first page and stores the term in the URL. Escape clears the filter.
Component State and Reload Cascades
A data-driven component holds two kinds of state. Design-time configuration, that is data source, page size, reload behaviour, display mode and field configuration, lives in the ComponentConfiguration and does not change during user interaction. Runtime state, that is the selected record, current page, sort key and filter text, lives in URL parameters and changes as the user interacts. Because runtime state is in the URL, it survives navigation and refresh.
When several components on a page share channels, a selection must trigger reloads in dependency order, otherwise a subscriber could render before its upstream channel has settled. Structr computes a dependency graph from the data source and channel relationships of all components on the page at render time and writes the reload order as an attribute on the active element. The frontend script reads it, resets the affected URL parameters in order and triggers the partial reloads. The data-channel attribute on a component’s root marks it as a listener for the named channels, and a component with reload behaviour others omits its own name so that it is not reloaded by its own actions.
Data Source Compatibility
When the data source, the transform or the expected type of a component changes, Structr checks the combination against the dimensions of the component and rejects it with an error if it does not fit. A transform can only be applied to a data source that delivers a single object, because a property cannot be navigated on a collection. A collection cannot be bound to a component with dimensions 0. When the transform points to a collection property, the result counts as a collection. Components without a dimensions value accept any data source. The check runs on every modification of the ComponentConfiguration, including deployment import, so a widget set with wrong dimensions fails early.
Data Source Names
The dataSource property holds a name that Structr resolves at render time. node:<Type> delivers all instances of a type, node:<uuid> a user-defined script-based or query-based data source by id, channel:<name> the record selected on a channel, parent the record that a surrounding repeater or component provides under the data key parent, and root-folders the root folders of the file system. The data source menu in the dialogs produces exactly these names.
Authoring Render Templates
A render template is a Widget with “Is Render Template” set. Structr instantiates it on demand: the first time a component needs the template, Structr expands the Widget source into a Shared Component in the hidden document, and reuses that Shared Component afterwards. Users never insert render templates.
Structr finds the instantiated Shared Component by the name of the render template, so the root element of the source must carry data-structr-meta-name with the Widget’s name. Without it, Structr creates a new instance on every render.
The selectors of a render template hold the data types it can display, for example date, string, boolean, node, enum or custom. The template dropdown of a field offers only templates whose selectors contain the field’s data type.
Inside the template, value holds the field value and field gives access to the field metadata: field.label, field.propertyName, field.required, field.options for enum and relationship fields, field.multiple, field.rows and field.config for the parameters described next. The textfield template of the default set renders:
<input class="sw-input" name="${field.propertyName}" placeholder="${field.label}"
required="${is(field.required, 'required')}" type="text" value="${value}"
data-structr-meta-name="textfield" />
A render template can declare parameters in its Configuration tab. The format differs from the template variables of other Widgets: each parameter has a label, an optional type (page or boolean, otherwise a text input), optional options, an optional comment and an optional defaultValue. The parameters appear in the Render Template Settings of every field that uses the template, their values are stored per field and read via field.config. The formatted-date template declares:
{
"dateFormat": {
"label": "Date Format (ISO 8601)",
"defaultValue": "dd.MM.yyyy"
}
}
and renders ${dateFormat(value, field.config.dateFormat)}. The related-link template declares a detailPage parameter of type page and a labelProperty parameter and renders a link to the detail page of the related record with the chosen property as link text.
Process-Bound Widgets
With the process module installed, a Widget can bind the component it creates to a user task of a BPMN process. The configuration uses the process and user-task field types so that the user picks the task on insertion, and a _autoVisibilityMapping meta entry whose values reference those fields in square brackets, for example "boundProcess": "[process]". On insertion, Structr resolves the references and creates a visibility mapping that shows the component only while the task is active. The binding mode of the ComponentConfiguration is then processBound, the data source is fixed to the current channel, and the subject type of the process appears as the read-only Expected Type. The Process Subject Form of the default set is an example. Processes and their pages are covered in the BPMN Process Control chapter.
Prefetching
Structr’s Neo4j Bolt driver includes an automatic prefetching system that optimizes graph traversals by reducing the number of individual database queries. When your application accesses relationships on many nodes of the same type, prefetching detects this pattern and loads the relevant subgraph in a single bulk query, caching the results in memory for the duration of the transaction.
This is similar in concept to the N+1 query problem in relational databases, where iterating over a list of objects and accessing a related object on each one results in one additional query per item. Prefetching solves this by recognizing repetitive query patterns at runtime and replacing them with a single, broader query.
How It Works
Prefetching operates at three levels: automatic detection and learning, manual triggering via scripting, and self-optimization through query combining.
Automatic Prefetching
During a transaction, Structr monitors every relationship query that passes through the Bolt driver. Each query is recorded in a histogram that tracks the Cypher statement pattern, the source node type, the relationship type, the direction, and a counter of how often it has been executed.
When the counter for a given pattern exceeds a configurable threshold (default: 100 executions within a single transaction), the system checks whether activating prefetching for that pattern is safe. It runs a count() query to estimate the result size. If the result count is below the maximum allowed (default: 50,000), the pattern is registered for prefetching. If it exceeds the limit, the pattern is blacklisted instead, preventing it from being considered again.
The learned patterns are associated with a “prefetch hint” - an identifier set by higher-level application code that describes the context of the current operation, such as rendering a specific page or processing a REST request. On subsequent transactions with the same hint, all learned patterns are executed immediately at the start, populating the cache before application code begins accessing relationships.
Patterns that take longer than the maximum allowed duration (default: 1,000 ms) are also blacklisted automatically.
Manual Prefetching
You can trigger prefetching explicitly from StructrScript or JavaScript using the prefetch() function. This is useful when you know in advance which part of the graph your code will traverse.
StructrScript:
${prefetch('(:Customer)-[]->(:Task)', merge('HAS_TASK', 'ASSIGNED_TO'))}
JavaScript:
$.prefetch('(:Customer)-[]->(:Task)', ['HAS_TASK', 'ASSIGNED_TO']);
The first argument is a Cypher path pattern describing the subgraph to load. The second argument is a list of relationship type keys that should be considered prefetched after the query completes. This tells the caching layer that it does not need to make additional database queries for these relationship types on the affected nodes.
Manual Prefetching with prefetch2()
The prefetch2() function provides more control over prefetching by allowing you to write a custom Cypher query that returns explicit node and relationship collections, and by separating outgoing and incoming relationship keys.
JavaScript:
$.prefetch2(
'MATCH (n:Customer)-[r:HAS_TASK]->(m:Task) RETURN collect(n) + collect(m) AS nodes, collect(r) AS rels',
['Customer/all/OUTGOING/HAS_TASK'],
['Task/all/INCOMING/HAS_TASK']
);
StructrScript:
${prefetch2(
'MATCH (n:Customer)-[r:HAS_TASK]->(m:Task) RETURN collect(n) + collect(m) AS nodes, collect(r) AS rels',
merge('Customer/all/OUTGOING/HAS_TASK'),
merge('Task/all/INCOMING/HAS_TASK')
)}
The arguments are:
- A Cypher query that returns two collections:
nodes(all nodes in the subgraph) andrels(all relationships). Unlikeprefetch(), which expects a path pattern, this gives you full control over the query structure. - A list of outgoing relationship keys that should be marked as prefetched on the source nodes.
- A list of incoming relationship keys that should be marked as prefetched on the target nodes.
- An optional ID parameter that is passed to the Cypher query as
$id. This allows you to scope the prefetch to a specific starting node.
The key format for relationship keys follows the pattern Type/all/DIRECTION/RELATIONSHIP_TYPE, for example Customer/all/OUTGOING/HAS_TASK.
The difference between the two functions is that prefetch() works with path patterns and treats all relationship keys symmetrically (both outgoing and incoming get the same keys), while prefetch2() lets you write arbitrary Cypher queries and assign different keys to outgoing and incoming sides. Use prefetch2() when you need fine-grained control over what gets cached, or when your subgraph structure does not fit a simple path pattern.
Query Optimization
When a transaction closes, Structr analyzes the collected prefetching patterns and attempts to combine them to reduce the number of queries on future transactions.
Combining by type and direction. If multiple patterns share the same source node type and direction but differ in relationship type, they are merged into a single pattern using Neo4j’s pipe syntax. For example, two separate patterns like (n:Task)-[r:HAS_SUBTASK]-> and (n:Task)-[r:HAS_ASSIGNEE]-> would be merged into (n:Task)-[r:HAS_SUBTASK|HAS_ASSIGNEE]->.
Combining by inheritance. If two patterns differ only in their source node type and those types share a common base type in the schema, the patterns are merged using the common ancestor. This further reduces the number of prefetch queries needed.
Caching Mechanism
When a prefetch query executes, it runs a MATCH p = (pattern) RETURN p query against Neo4j that returns complete paths. The driver iterates over each path segment and populates two layers of cache:
The transaction-level cache stores NodeWrapper and RelationshipWrapper objects keyed by their Neo4j internal IDs. Any subsequent access to these entities within the same transaction returns the cached wrapper without hitting the database.
Each NodeWrapper maintains its own relationship cache organized as a tree structure. Prefetched relationships are inserted into this cache, and the node records which relationship keys have been prefetched. When application code later calls getRelationships() on a node, the system checks this cache first. If data is present, it is returned directly. If a key is marked as prefetched but no relationships exist for it, an empty result is returned immediately. This is an important optimization, because it avoids a database round-trip to confirm that no relationships exist.
Any write operation (creating or deleting a node or relationship) within the transaction invalidates the prefetch caches, forcing subsequent reads to go to the database again. This ensures data consistency within the transaction.
Configuration
Prefetching behavior is controlled by four settings in the “Prefetching” section of the database configuration.
| Setting | Key | Default | Description |
|---|---|---|---|
| Prefetching Threshold | database.prefetching.threshold | 100 | The number of times an identical query pattern must execute within a single transaction before automatic prefetching is activated for that pattern. |
| Max Duration | database.prefetching.maxduration | 1000 | Maximum time in milliseconds that a prefetch query may take. Patterns that exceed this duration are blacklisted and will not be prefetched again. |
| Max Count | database.prefetching.maxcount | 50,000 | Maximum number of results a prefetch pattern may return. Patterns exceeding this count are blacklisted to avoid loading excessively large subgraphs into memory. |
| Cost Ratio | database.prefetching.costratio | 100 | How many entities a prefetch query may load per lookup that it saves in the transaction. A prefetch that loads more than that is blacklisted for the request it was learned for, because loading the graph costs more than the lookups it replaces. |
These settings can be adjusted in the Configuration Interface or directly in structr.conf.
Debugging
To monitor prefetching activity, enable Cypher debug logging by setting log.cypher.debug to true in your configuration. When enabled, the system logs:
- When a pattern is activated for prefetching, including the pattern and the threshold that triggered it
- When a pattern is blacklisted because it returns too many results, takes too long or loads more entities per saved lookup than the cost ratio allows
- The number of entities prefetched and the time taken for each prefetch operation
- A query count histogram at the end of each transaction, showing which patterns ran and how often
This information helps you understand whether prefetching is working effectively for your application’s access patterns and whether any of the thresholds need adjustment.
When Prefetching Helps
Prefetching is most effective in scenarios where your application iterates over a collection of nodes and accesses the same type of relationship on each one. Typical examples include rendering a list view where each item displays related data, processing a batch of records that each reference linked entities, or traversing a tree structure where every node has children of the same type.
If your application’s access patterns are highly varied – accessing different relationship types on different node types in an unpredictable order – the automatic detection may not activate because no single pattern reaches the threshold. In these cases, manual prefetching with the prefetch() function gives you explicit control.
Related Topics
- Configuration - General configuration settings
- Best Practices - Application development guidelines
- Logging & Debugging - Monitoring and troubleshooting