AEM Guide

What Actually Belongs in the ui.apps Module (and What Doesn't)

A field guide to what an AEM project's ui.apps Maven module should ship — components, clientlibs, i18n dictionaries — and why authored content never belongs there, with the FileVault filter.xml rules that decide what gets overwritten or deleted on deploy.

ui.appsfilevaultmavencontent-packagecloud-manager

ui.apps is the Maven module in an AEM project that builds a FileVault content package installing into /apps. That much every AEM developer knows. What causes real incidents is less obvious: exactly which nodes are allowed to live there, and what the package’s filter.xml does to anything else it finds under those paths at deploy time. Get the scope wrong and a routine ui.apps deploy can silently delete content that has nothing to do with your change.

This article assumes you already know the standard multimodule layout (core, ui.apps, ui.content, ui.frontend…) — it focuses only on what ui.apps should contain and how its FileVault filter controls what happens on install.

What ui.apps actually ships

ui.apps is a packageType=application content package. In AEM as a Cloud Service, an application package is only allowed to touch /apps — never /content, /conf, or any other runtime-writable area. What belongs under /apps is, by definition, code and configuration authored by developers and shipped through CI/CD, not content authored by editors in the AEM UI:

One thing that surprises teams coming from /apps-centric thinking: editable template definitions and their policies do not ship from ui.apps, even though they feel like “code.” A cq:Template and its cq:Policy nodes live under /conf/mysite/settings/wcm/..., and /conf is a mutable, author-editable path — policies in particular are routinely edited by authors through the “Edit Template” UI in production. Since an application package can only touch /apps, templates and policies have to ship from a content-type package (usually ui.content, or a dedicated ui.config/structure module), typically with mode="merge" so a redeploy doesn’t stomp on policy tweaks an author made after go-live. What genuinely belongs to ui.apps from the template world is the structure component’s code — the HTL/Java backing the layout container — not the template or policy node itself.

Why authored content must never live in ui.apps

Pages under /content, DAM assets under /content/dam, and tags used to classify that content are ui.content’s job, never ui.apps’s — and this isn’t just a style preference, it’s enforced by AEM as a Cloud Service: a single content package cannot deploy to both /apps and a runtime-writable area like /content at all. But even short of that hard rule, mixing the two causes real damage:

filter.xml: what a filter root actually controls

Every content package — ui.apps included — declares its scope in META-INF/vault/filter.xml using <filter root="..."> elements. A filter root is not a hint about “where this package mostly puts stuff”; per the Apache Jackrabbit FileVault documentation, it defines the subtree the package owns for the purposes of import:

That single fact — uncovered-but-declared paths get deleted, not ignored — is the mechanism behind almost every “a deploy wiped out content that had nothing to do with my change” incident.

A realistic ui.apps filter.xml

<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
    <filter root="/apps/mysite/components"/>
    <filter root="/apps/mysite/clientlibs"/>
    <filter root="/apps/mysite/i18n"/>
</workspaceFilter>

Three narrow, explicit roots — each one matching exactly the subtree this module is responsible for. Nothing here claims /apps wholesale, and nothing here reaches into /conf or /content.

The too-broad mistake

<filter root="/apps"/>

This looks like a harmless shortcut — “we own /apps/mysite, and /apps/mysite is under /apps, so why not.” But the filter root is /apps itself. On install, FileVault now considers the entire /apps tree — including AEM’s own Core Components under /apps/core, another team’s package under /apps/othersite, anything — to be covered by this package. Since your package’s jcr_root only actually contains apps/mysite/..., everything else under /apps is “covered but not contained,” and gets deleted on import. This is precisely how a routine ui.apps deploy takes down Core Components or a sibling application on a shared AEM instance.

The too-narrow mistake

<filter root="/apps/mysite/components"/>
<!-- someone adds /apps/mysite/templates/structure locally and forgets
     to add a filter root for it -->

The content-package-maven-plugin builds the package strictly from what filter.xml covers. A folder added under jcr_root that isn’t under any declared filter root is simply excluded from the built package — no warning, no build failure. It’s committed to git, it exists on disk, and it never reaches the target instance. This is the quieter failure mode: nothing breaks visibly, a feature just never shows up, and the fix is usually “someone forgot to add a <filter root> line.”

Where this comes up