10.0 Changelog

This page lists all the changes introduced in 10.0 releases. See also the release notes and the migration guide.

10.0.0 (2026-10-23)

Feature

  • Add the workflow automation engine

    Details

    Added a lightweight, declarative workflow engine bound to models: a workflow starts from one trigger (create, update, timer, manual, or a message another workflow sends) and walks a graph of nodes until an end, covering both automation and human-approval flows. Workflows are authored as XML under src/main/resources/workflows/, validated against an XSD plus a graph-wide validator, then loaded into MetaWorkflow records the same way views and actions are. Everything runs after the triggering transaction commits, so a workflow failure never rolls back the user’s save.

    Node types: script (inline JavaScript against the record), agent (ask an AI agent, optionally checking its answer against a declared shape), email (send an email template), split and join (branch on one, some or all flows and wait for all, any or a quorum of them), call (run another workflow, once or per record), subflow (a region of the same workflow, once or per record), task (a human approval with an assignee, a user, or a role or group pool), wait (park on a timer, a condition, a message or a signal), send (announce a message or broadcast a signal), boundary (a deadline, condition, message, signal or error attached to another node) and end.

    An agent or email node runs in the background: the run stops there, and the node runs on a worker of its own once everything before it has committed, so a slow model or mail server neither holds up nor undoes the save. A slow script opts in with async="true", and any of the three opts out with async="false". The record’s form shows such a run as pending and reloads the record once the node commits, so an edit made meanwhile does not collide with the newer version.

    Pending tasks surface in a My Approvals inbox and on the record form’s toolbar, both permission-checked per task. A workflow’s id is its stable identity, and each run is pinned to the revision it started on, so parked runs and tasks survive undeploy and redeploy.

    Execution history is kept at three levels, set by workflow.trace: off records failures, runs adds a row per run, and nodes adds a row per node visited. Parks are recorded at every level. A run form lists the steps it took, and a Node Metrics view groups them by node across runs.

    Workflows can also be built visually in a Studio designer that saves and deploys through the same validation, and authored from chat through a Studio tool exposed to the AI assistant.

  • Resolve the client IP address from a single trusted header

    Details

    Behind the proxy filter, HttpServletRequest#getRemoteAddr() now returns the client IP address resolved from a single header: the left-most address of the header configured with the new application.client-ip-header setting, or of X-Forwarded-For by default. The address is validated and normalized, ie any port suffix is stripped and IPv6 addresses are canonicalized, whereas the left-most X-Forwarded-For value was previously used as it is.

    If that header is missing, or doesn’t provide a valid IP address, the address of the direct connection is used. No other header is tried, such as X-Real-IP, as the reverse proxy may not overwrite it, so it may have been forged by the client.

    # header providing the client IP address, set by the reverse proxy
    application.client-ip-header = X-Real-IP

    The configured header is trusted as it is: the reverse proxy must overwrite it, and the application must only be reachable through the proxy. With nginx, using the default X-Forwarded-For header:

    proxy_set_header X-Forwarded-For $remote_addr;

    Don’t use $proxy_add_x_forwarded_for: it keeps the X-Forwarded-For value sent by the client, whose left-most address would then be used as the client address.

  • Add typed JSON extract functions for longs, dates and times

    Details

    PostgreSQL gains the JPQL functions json_extract_long, json_extract_date, json_extract_datetime and json_extract_time, so a long, date, date-time or time held in a JSON field can be read with its type and passed to date functions. A JSON path names them with ::long, ::date, ::datetime and ::time; ::long previously read as a 32-bit integer.

  • Add advanced search support to record selector popup

    Details

    The record selector popup (opened from many-to-one/many-to-many/one-to-many select actions) now shows an advanced search bar in its header, letting users build custom search criteria over the selector’s grid columns instead of relying only on the plain free-text search.

  • Give notification popups more time and a way to keep them open

    Details

    The notification popups shown in the bottom corner disappeared after three seconds, which left no time to read a long message, let alone copy it, and no way to keep one on screen.

    Popups now last five seconds, and stay for as long as the pointer rests on them or the keyboard focus is inside them. The five seconds start over once the pointer leaves.

    Every popup also carries a close button, so it can be dismissed on the spot instead of waiting for it to fade.

  • Add ReportConfigurer to customize report generation

    Details

    ReportGenerator now delegates application specific behavior to an injectable ReportConfigurer:

    • validate(report, params): called once the design is opened, throw to abort the generation

    • configure(task, options, params): called before the task runs (timezone, render options…​)

    Override ReportConfigurerImpl instead of extending ReportGenerator, so that report generation fixes and improvements still apply to your application:

    bind(ReportConfigurerImpl.class).to(MyReportConfigurer.class);
  • Add FieldSelector component for browsing model fields in grid view customizer

    Details

    A new FieldSelector component allows users to browse and select fields from a model, including traversal through relational fields (many-to-one, one-to-one), producing a dotted field path (e.g. partner.fullName).

    The component is integrated into the grid view customizer toolbar as an Add New action, allowing arbitrary dotted field paths to be added as columns without requiring them to be declared in the view XML.

    Key behaviors:

    • Fields are listed alphabetically and filterable via a search input.

    • Clicking a relational field drills into its target model; breadcrumb tags in the trigger reflect the current navigation path.

    • Keyboard navigation: ArrowUp/ArrowDown to move, ArrowRight to drill into a relational field, Enter to select, Backspace to navigate back.

    • Fields already fetched for a model are cached and not re-fetched on revisit.

  • Throttle refresh of API key lastUsedAt

    Details

    API key authentication is stateless, so it runs on every single request. The lastUsedAt of the API key was written on each of them, turning a read-only workload into one UPDATE plus one commit per request, and invalidating the cache regions of UserToken just as often.

    lastUsedAt is now refreshed at most once every 5 minutes. That interval is wide enough to tell active API keys from unused ones, which is what the field is for, while keeping clients making frequent calls from writing to the database on each of them. Once the interval has elapsed, a lease of 30 seconds makes sure a single request refreshes the timestamp, instead of every request that was already in flight.

    Both mechanisms prevent row-level lock contention and API response delay when dozens of concurrent requests use the same token.

  • Support Java text blocks in translatable messages

    Details

    Java text blocks can now be used for translatable messages, either with I18n.get() or with the /*$$(*/ ... /*)*/ marker, and are extracted by the i18n task:

    I18n.get(
        """
        <ul>
          <li>First line
          <li>Second line
        </ul>\
        """);
  • Log user logins

    Details

    Successful authentication attempts are now recorded as AuthenticationEvent. Users can see the history of their own attempts from the user and preferences forms. Admins can see the attempts of every user from the new "Connections" menu. Direct clients such as API key and basic authentication are excluded, and external providers only report their successful logins. Events older than 180 days are deleted daily by the new "authentication.events.purge" scheduled job. The retention period can be changed with the "auth.events.retention" setting.

  • Add the AI assistant

    Details

    A conversational AI assistant, available from every screen of an application. It answers questions, looks up and changes records through tools, draws on indexed knowledge, and can build the application with Studio. All of it is configuration under the AI menu: providers (OpenAI, Anthropic, Google, Mistral, Ollama or a custom endpoint) and their models, which can be synced from the provider; agents, each with a prompt, a model, a cheaper utility model for background work, and the tools, skills, knowledge bases and sub-agents it may use; tools of several kinds — system, client, script, HTTP, action, and those of external MCP servers; skills loaded on demand; and knowledge bases indexing documents and records. Everything seeds from files under a module’s ai/ directory and travels between databases in config bundles.

    Answers stream into the chat panel, keep running when the browser disconnects, and show each tool call as a card; a tool can ask for the user’s approval first. An answer can carry a chart, table, KPI or other data widget, drawn from a query run as the user. A conversation is pinned to the record it is about (/subject shows or changes it), can be asked about form fields picked on screen, and is summed up with /recap. Procedures let an agent change a record the way a user would, through the form’s own steps, and the Agent Builder creates and edits agents in conversation.

    A conversation can be shared: members follow it, post in it, or, with "Can ask", bring in the agent, whose answers show who asked. In assistant mode a plain message runs the agent; in discussion mode it goes to the members, and /discuss switches between them. A shared conversation marked as a space keeps its name and is offered again when sharing with the same people. The assistant button opens what is waiting: the one unread conversation, or the history filtered to the unread ones.

    The assistant never goes beyond the user’s own permissions, field rules included, on every path it reads or writes through: data tools, queries, procedures, the open form, mentions, recaps and record knowledge. A field that comes along unasked and that the user may not read is withheld and listed under $hidden, and a call with no user is refused. Within that, each agent lists the models its job needs and the operations allowed there (["* read", "Order read update"] in an agent file); an agent with none reaches no data, and a consulted agent is held to every agent before it. Data policies (AI › Configuration) say what the AI may never read — fields of a model, a whole model, or the records a condition covers — for every call when global, else for the agents, models or providers they list. A refusal names the operation and the model, and whether the user or the assistant lacks the access. A Studio tool needs the user’s own right to Studio’s resources.

    A conversation keeps the fields whose values entered it, and every model it is later sent to must be allowed to receive them: a turn on a model that may not is refused before any call, with the offer of a new conversation; a consultation of such an agent is skipped; a background call (title, summary, recap) falls back to the turn’s model, or is skipped. A record knowledge source declares the fields its template reads, and indexing refuses those a policy keeps from the embedding model.

    Usage policies cap the monthly chat tokens and the request rate of a role, group or user. Every model call, background tasks and embeddings included, is in the usage log with the fields it carried, or as REFUSED when it was not sent.

    The AI tables of the former Enterprise Edition changed: permissions on models replace an agent’s allowed_models, usage policies are named and list the roles, groups and users they apply to, and cross-session memories are removed. See the 10.0 migration guide for the SQL script.

  • Support a JWKS file for the OpenID Connect private key JWT client authentication

    Details

    The signing key of the private key JWT client authentication config can be read from a JWKS file:

    auth.provider.keycloak.client-authentication-method = private_key_jwt
    auth.provider.keycloak.private-key-jwt-client-authn-method-config.jwks.path = /path/to/jwks.json
    auth.provider.keycloak.private-key-jwt-client-authn-method-config.jwks.kid = my-key

    The validity, key-clock-skew and use-expiration settings are also supported.

    Configuring a private key with private-key.path, private-key.algorithm, jws-algorithm and key-id is no longer supported. See the migration guide for more details.

  • Add current tenant in logs

    Details

    When multi-tenancy is enabled, log lines didn’t tell which tenant they belong to, making it hard to troubleshoot an issue for a given tenant in the shared log file.

    A new %tenant pattern converter outputs the tenant identifier, with an optional fallback used when no tenant is set on the thread, e.g. %tenant{-}. The raw identifier is also available as %X{tenant} for custom logback.xml configurations.

    When multi-tenancy is enabled, the LOG_TENANT_PATTERN variable is set to [%tenant{-}] +`. Default console and file patterns include it as `${LOG_TENANT_PATTERN:-}+ right before the thread name. When disabled, it is empty and the log format is unchanged:

    ... INFO 973172 --- [acme] [http-nio-8080-exec-1] com.axelor... : ...
    ... INFO 973172 --- [-] [main] com.axelor... : ...
  • Allow restricting API keys to IP addresses or CIDR ranges

    Details

    API keys can now be restricted to a comma-separated list of IP addresses or CIDR ranges (IPv4 and IPv6), from the API key form. A restricted API key used from any other address is rejected. An API key without any restriction keeps working from any address.

    A new allowed_ips column is added to the auth_user_token table:

    ALTER TABLE auth_user_token ADD allowed_ips varchar(2048);

Change

  • Upgrade backend dependencies

    Details

    Here is the list of backend dependencies upgraded:

    • Gradle from 8.14.5 to 9.8.2

    • HikariCP from 7.0.2 to 7.1.0

    • Logback from 1.5.38 to 1.6.5

    • Caffeine from 3.2.4 to 3.3.1

    • Guava from 33.5.0 to 33.7.2

    • CommonsIO from 2.21.0 to 2.22.0

    • Apache Tika from 3.2.3 to 3.3.2

    • Snakeyaml from 2.6 to 2.7

    • Birt from 4.23.0 to 4.25.0

    • ASM from 9.9.1 to 9.10.1

    • Woodstox from 7.1.1 to 7.2.2

    • Junit from 6.0.3 to 6.1.3

    • Jline from 4.1.3 to 4.4.8

    • Redisson from 4.7.0 to 4.8.0

    • Byte Buddy from 1.18.14 to 1.18.15

    • Tomcat from 10.1.60 to 10.1.61

    • PostgreSQL JDBC Driver from 42.7.13 to 42.7.14

  • Improve JavaScript scripting performance and result durability

    Details

    JavaScript action-scripts and expressions now run faster and return durable results. The wall-clock script timeout is enforced with a lightweight interrupt watchdog instead of attaching a per-evaluation execution listener (which GraalVM documents as expensive and able to deoptimize compiled code); the configured timeout and the resulting ScriptTimeoutException are unchanged. Script results that are arrays or objects are now returned as plain, durable Java List/Map values instead of live context-bound proxies, so they remain valid after the script helper is closed. A cached compiled-function API (execute(Source, scope) with named sources) was added for callers that run the same script repeatedly. JavaScript Map and Set results are returned as Map/List of their entries, integers too large for long as BigInteger instead of a lossy double, and a Date as the Instant it stands for instead of a LocalDate, which lost its time and took the day of the JVM’s time zone.

  • Resolve bare type names to host types in JavaScript actions

    Details

    In JavaScript action-scripts and expressions, a bare type name (a model, repository, or built-in helper like LocalDateTime, BigDecimal, Arrays) now resolves to a GraalVM host type instead of a java.lang.Class instance. Static members and constructors now work everywhere, e.g. LocalDateTime.now(), Arrays.asList(1, 2, 3), new BigDecimal('1.50') and new Contact().

    The type stays usable as a Class argument (__repo__(Contact), __bean__(MyService), context.asType(Contact)) and with instanceof (record instanceof Contact), because GraalVM unwraps a host type to its java.lang.Class when passed to a Class parameter. Only the JavaScript engine is affected; Groovy scripts are unchanged.

    Breaking changes: java.lang.Class instance methods are no longer callable on a bare type name. Reach the Class through .class, or use the equivalent operator:

    • Contact.getName() → Contact.class.getName()

    • Contact.getSimpleName() → Contact.class.getSimpleName()

    • Contact.isInstance(record) → record instanceof Contact

    • Contact.isAssignableFrom(other) → Contact.class.isAssignableFrom(other)

    Finding them: this is rare and only affects entity (model) class names. Any such call now fails loudly at run time with TypeError: ... Unknown identifier: <method>, naming the exact call. To audit ahead of time, search where scripts live — action XML files, and action definitions stored in the database (meta_action.xml) — for a capitalized receiver calling one of these methods:

    grep -rEn '\b[A-Z][A-Za-z0-9_]*\.(getName|getSimpleName|getCanonicalName|isInstance|isAssignableFrom)\(' .

    Most matches are calls on ordinary objects; only the ones whose receiver is an entity/model class name need changing. Fix them by hand using the mappings above — there will be few.

  • JSON fields performance improvements

    Details

    Performance improved by caching results from MetaStore#findJsonFields(). This improves custom model data import performance and other JSON fields operations.

    JsonProperty.getTitle() now returns untranslated title, to realign consistency with Property.getTitle().

    Context no longer applies user permission/visibility filtering when checking whether a JSON field exists, to realign consistency with how regular (non-JSON) fields are resolved.

    JSON field and JSON model changes are reflected by MetaStore only once the transaction completes. Reading them back within the same transaction may return the previously cached state.

  • List many-to-many values inline in advanced export and import

    Details

    Advanced export writes a many-to-many field, or a collection custom field, as one column of its owner, the related records' keys separated by | (a | or \ inside a key is escaped with \). It does so in both standard and flat export, so a flat export can now include the main model’s many-to-many fields. Link sheets such as OrderLine.taxes are no longer exported unless the Link sheets option is chosen, which a flat export does not offer.

    On import, such a cell is the record’s complete set: values left out are unlinked, a blank cell clears the collection, and a file without the column leaves it unchanged. Link sheets exported by earlier versions still import, and only add links.

    Import now orders the sheets of a file by the records they reference, and sets references to records created later in the same sheet, or around a cycle across sheets, in a second pass. A reference that still names no record is reported in the result’s Errors sheet, and its record is kept without it. A cycle made only of required references can’t be imported and is refused before anything is written.

    Many-to-many changes are now written through the ORM rather than directly to the join table, so auditing, workflows and cached collections see them.

  • One edition under the Axelor Commercial License

    Details

    Axelor Open Platform no longer has separate Community and Enterprise editions. All features of the Enterprise Edition are now part of the platform: AI assistants and agents, Studio, global search, real-time collaboration on views, data import/export, and additional SSO settings (provider order, default provider, local and central logout).

    The whole platform is now distributed under the Axelor Commercial License, instead of the AGPL v3 for the former Community Edition. See the LICENSE file.

    The axelor-enterprise-edition module and the axelor-core-enterprise and axelor-web-enterprise artifacts are no longer published: their content is now in axelor-core and axelor-web. The axelor.platform.ee Gradle property is no longer used.

    Former Enterprise* classes are now merged into their base classes or renamed, such as EnterpriseAvailableAppSettings into AvailableAppSettings and EnterpriseAppFeatures into AppFeatures.

    See the 10.0 migration guide for the upgrade steps.

  • Improve @DynamicUpdate support

    Details

    Currently, @DynamicUpdate is added on entity having a computed persisted field. The annotation prevents using cached PreparedStatement but it generates a new SQL string specifically for that transaction, compiles it, and executes it.

    We now apply this annotation only when the cost of generating the SQL is outweighed by the savings in database I/O. Cases are :

    • large tables (more than 100 XML-declared column-backed fields): prevent massive, unnecessary data payloads

    • tables with large objects (binary/large fields): ensure heavy binary/large data is not re-transmitted unless it is actually changed

    Also, allow enabling/disabling the @DynamicUpdate support on demand per entity through the dynamicUpdate attribute for more control.

    JSON fields like attrs do not trigger @DynamicUpdate automatically because their runtime size is unknown and most entities leave attrs empty. Users should set dynamicUpdate="true" explicitly on entities with large JSON payloads.

  • Promote command line application to GA

    Details

    The command line application is no longer in beta.

    The distribution archives are now built as part of the standard Gradle lifecycle: build (and assemble) produce both the ZIP and TAR archives in build/distributions/. Previously, they were only generated when distZip, distTar or assembleDist were explicitly requested.

    The extracted distribution can still be generated in build/install/ with the installDist task.

Remove

  • Remove support of context.appLogo and context.appIcon methods without mode parameter

    Details

    The methods referenced by the context.appLogo and context.appIcon settings must now accept a single String mode parameter (light or dark). Methods without parameter, deprecated since 7.3, are no longer called and the default application logo/icon is used instead.

  • Remove deprecated sequential attribute on entities

    Details

    The sequential attribute on <entity> definitions, deprecated and ignored since 8.0, is removed from the domain models schema. Each entity uses its own dedicated sequence. Remove the attribute from domain XML files.

  • Remove @DynamicInsert support

    Details

    @DynamicInsert on entities provides almost zero benefit. In fact, it often decreases performance because it prevents PreparedStatement caching. The support is removed. No changes are expected.

  • Remove deprecated TenantAware and AuditableRunner

    Details

    com.axelor.db.tenants.TenantAware and com.axelor.auth.AuditableRunner, deprecated since 8.0, are removed. Use ContextAware to build a ContextAwareRunnable or ContextAwareCallable instead. See the migration guide for details.

Fix

  • Fix the tenant host changed for the rest of the request after a synchronous context-aware run

    Details

    Running a ContextAware task on the calling request thread now restores the tenant host taken from the request. It was replaced by the first host configured for the tenant, or cleared when the tenant has no configured hosts, which affected tenant base URLs built later in the same request.

  • Fix report data sets failing with "Connection is closed"

    Details

    Reports run outside a transaction lent the request’s database connection to BIRT. Any entity query made while rendering, such as ImageTool loading a company logo, released that connection to the pool. The data sets queried afterwards failed with Connection is closed, and the report was generated empty or incomplete.

    Outside a transaction, reports now run on a dedicated database connection. Within a transaction, they still use the transaction’s connection and see its uncommitted changes.

  • Fix collaboration user names when User namecolumn is redefined

    Details

    When the namecolumn of the User entity was redefined (for example to fullName), the collaboration avatars shown when several users open the same record had no initials, and their tooltip showed "undefined" instead of the user’s name. They now show the user’s initials and name again.

  • Fix global search indexer using the batch size as its thread count

    Details

    The mass indexer read its thread count from global.search.indexer.batch-size, so global.search.indexer.threads had no effect. Setting a batch size of 500 started 500 threads. The indexer now reads global.search.indexer.threads.

  • Fix adding a custom model record from an editable grid

    Details

    In the editable grid of a custom model, adding a new row and saving it failed with "jsonModel - must not be null". New rows now get the custom model set, as new records created from the form view already did.

  • Fix save data on m2m grid reordering

    Details

    Unlike o2m grid, when move operation is performed on m2m grid then it should directly saved changed order of records to server.

  • Fix editable grid wiping custom field values

    Details

    In an editable grid showing custom fields, whether on a custom model or on a regular entity, leaving a row right after clicking into it could save the record with all its custom field values emptied, and with its name emptied on a custom model. The grid now keeps the row’s values, and no longer saves a row that was left unchanged.

  • Fix saving a new user

    Details

    Creating a user failed on save, because the MFA configuration of the user was looked up before the user was persisted. New users are now saved without error.

  • Fix error after multi-factor authentication when login was opened directly

    Details

    When the login page was opened directly instead of through a redirect from another page, completing multi-factor authentication ended on an "Unexpected Application Error" (Cannot read properties of undefined (reading 'pathname')). The user is now redirected to the home page.

  • Fix websocket handshake refused behind a proxy forwarding the wss scheme

    Details

    Without application.base-url, the origin check of the websocket handshake refused every connection behind a reverse proxy that forwards the upgrade request with X-Forwarded-Proto: wss (or ws), such as Traefik: the base url was built with the wss:// scheme while the browser sent an https:// origin. The ws and wss schemes are now compared as http and https.

  • Fix add item in movable top-level editable grid

  • Fix the user leaking to the next request after a synchronous context-aware run

    Details

    Running a ContextAware task on the calling request thread no longer leaves the session user in the current-user thread-local. The next request served by the same pooled thread could otherwise run as the previous caller.

  • Fix realtime features stopping after the websocket connection drops

    Details

    After the websocket connection dropped, for example on a network outage or a server restart, it was not reconnected until a message had to be sent, and the reconnected socket did not subscribe to its channels again. The connection is now re-established automatically with an increasing delay, and its channels and rooms are restored when it reopens.

    A failing open or close callback no longer stops the other channels from being restored, and a failing message listener no longer keeps the message from the other listeners.

  • Fix setting the OpenID Connect private key JWT client authentication config

    Details

    Since pac4j 6.4.0, the private-key-jwt-client-authn-method-config setting of an OpenID Connect provider couldn’t be set anymore.

    The return type from OidcConfiguration.privateKeyJwtClientAuthnMethodConfig was changed from PrivateKeyJWTClientAuthnMethodConfig to IPrivateKeyJwtClientAuthnMethodConfig.

  • Fix JavaScript action-scripts failing with "The Context is already closed"

    Details

    A JavaScript <action-script> that set a value from an object or array literal, such as $response.setValue('customer', {id: 1}), failed with The Context is already closed. The literal was kept as a proxy bound to the script’s context, which was closed before the response was sent to the client. Response values are now copied into plain Java values before the context is closed.

  • Do not apply field domain when searching JSON relational references on import

    Details

    When importing data (CSV/XML), searching for an existing custom (JSON) record to reference is now scoped only by self.jsonModel, no longer by the JSON field’s domain. The field domain should be a UI selectability constraint only.

  • Fix values lost when chaining actions on a saved record

    Details

    On a saved record, when several actions were chained together (for example in an action-group or an onChange), the values set by some of them were lost if one of the actions returned the whole record and another set only a few fields. The form kept its stale values and was not marked as modified. All values returned by the chained actions are now applied, the later action winning for a given field.

Security

  • Validate temporary file names

  • Serve report files by token

    Details

    The ws/files/report endpoint, which accepted a temporary file name with the link parameter, has been removed. Report files are now served by the ws/files/data-export endpoint with a token parameter, provided by the new ActionResponse.setFile method and by action-report. A report token is only valid for the user who created it, and expires after 5 minutes.

    ActionResponse.setFile replaces the ActionResponse.setExportFile methods. It takes a ResponseFile, which the client opens in a new tab for preview if created with ResponseFile.preview and supported by the browser, or always downloads if created with ResponseFile.download:

    // Before
    response.setView(ActionView.define(title).add("html", fileLink).map());
    response.setExportFile(path, fileName);
    
    // After: open the report in a new tab
    response.setFile(ResponseFile.preview(path, fileName).title(title));
    
    // After: download the file
    response.setFile(ResponseFile.download(path, fileName));
  • Serve data export and import result files by token

    Details

    The ws/data/download and ws/rest/{model}/export/{name} endpoints, which accepted a temporary file name, have been removed. The ws/data/export, ws/data/import and ws/rest/{model}/export endpoints now return a token to download the file with the ws/files/data-export endpoint.

    Pending export files (ws/data/export, ws/data/import, ws/rest/{model}/export, ActionResponse.setFile and action-export) are now only accessible by the user who created them.