Naming conventions

Updated July 2026

Naming conventions define how every asset in your project is named and where it lives, both in the engine and on disk. You author one document per project; Dousen compiles it and serves the result to every DCC addon and the Unreal plugin, so a name assembled in Blender matches what Unreal expects without anyone copying rules by hand.

How it works

  1. You author the document in the web portal — either as YAML (the default) or as a Lua script for computed rules (see below). Editing requires the admin role.
  2. The server compiles it: the taxonomy tree is resolved, every node gets its full effective validator set baked in, and errors (bad regex, malformed structure) are caught.
  3. Clients consume the compiled form from GET /api/projects/:id/conventions/compiled and cache it for 5 minutes — after publishing a change, expect up to that long before every open DCC picks it up.

The current schema is version 3. Its core idea: names are assembled structurally from prefixes, suffixes, and dimensions — not from a single text template.

A complete example

YAMLversion: 3

dimensions:
  variant:
    required: false
    values: [Base, Hero, Damaged]
  lod:
    required: false
    values: [LOD0, LOD1, LOD2]
  number:
    required: false
    values: []          # empty = free-form

validators:             # global — apply to every asset
  - id: naming.convention
    severity: error
  - id: naming.no_special_chars
    severity: error

asset_types:
  StaticMesh:
    prefix: SM
    suffix: ""
    naming:
      pattern: "^SM_[A-Za-z0-9_]+$"   # guard rail the final name must satisfy
      max_length: 64
    source_path:
      mirrors_engine: true
    validators:                        # scoped to this asset type
      - id: mesh.poly_count
        severity: warning
        params: { max_tris: 50000 }
    categories:
      Props:
        folder: true
        subcategories:
          Weapons:
            folder: true
            prefix: Wep
            validators:                # scoped to this node + everything below
              - id: mesh.poly_count
                severity: error        # tightens the asset-type rule
                params: { max_tris: 20000 }
            subcategories:
              Melee:
                folder: true

With that document, a Static Mesh named Sword at Props/Weapons/Melee with variant Hero resolves to the name SM_Wep_Sword_Hero and the engine folder Props/Weapons/Melee.

Keyword reference

Top level

KeyTypeRequiredNotes
versionintyesMust be 3. Missing or any other value is a compile error.
dimensionsmapnoOrthogonal naming axes appended to names. Default {}.
validatorslistnoGlobal rules applying to every asset. Default [].
asset_typesmapnoOne entry per asset type. Default {}.

dimensions.<name>

KeyTypeDefaultNotes
requiredboolfalseAdvisory — surfaced to client UIs; not enforced at name resolution.
valueslist of strings[]The pick-list shown in DCC "Set Up Asset" UIs. Empty means free-form. Membership is not enforced at resolution.

Dimension keys are open — you can declare any — but only variant, lod, and number participate in name assembly, in exactly that order. A custom dimension appears in client UIs and convention tags but is never appended to the name. variant is dropped from the name when its value is exactly Base.

Validator entries

The same entry shape is accepted at all three scopes — global, asset type, and taxonomy node. Placement is the scoping; there is no applies_to field. See Asset validation for rule semantics and the catalog.

KeyTypeDefaultNotes
idstringrequiredRule id, e.g. mesh.poly_count. Unknown ids are allowed (a DCC that doesn't have the rule skips it).
severitystringerror blocks export, warning is advisory.
enabledbooltrueSet false to switch a rule off at this scope (and below).
paramsmap{}Rule-specific parameters. When a deeper scope redeclares a rule, its params are merged key-by-key over the shallower ones — so a node can tighten just max_tris without restating everything.
name / categorystringOptional display metadata; the portal fills these from the catalog.

asset_types.<name>

KeyTypeDefaultNotes
prefixstring""First token of every name of this type (e.g. SM).
suffixstring""Last token of every name of this type.
naming.patternregexA guard rail the final assembled name must match — not a template. An invalid regex is a compile error naming the asset type.
naming.max_lengthintMaximum length of the final name.
source_path.template_overridetemplateWhere the working file lives when it doesn't mirror the engine path. See Source paths.
source_path.mirrors_enginebooltrueDerived, not read: it is true exactly when no template_override is present. Writing it explicitly is harmless documentation.
validatorslist[]Rules scoped to this asset type, merged over the globals.
categoriesmap{}The first level of the taxonomy tree.

subcategories directly under an asset type is a compile error — the first level must be categories; subcategories only appears on nodes inside it.

Taxonomy nodes (categories and subcategories)

Every node under categories has the same shape and may nest subcategories recursively to any depthProps/Weapons/Melee/Daggers/… is fine. An asset's place in the project is an ordered path of node names.

KeyTypeDefaultNotes
folderbooltrueWhen true, the node's name becomes a folder segment in the engine path. Set false for nodes that should only shape the asset name.
prefixstring""Name token contributed before the asset name.
suffixstring""Name token contributed after the asset name.
requiredboolfalseAdvisory flag for client UIs.
validatorslist[]Rules for this node and everything beneath it.
subcategoriesmap{}Child nodes — same shape, any depth.

How a name is assembled

Dousen joins the following tokens with _, skipping empty ones:

  1. the asset type's prefix
  2. each taxonomy node's prefix, walking the path root to leaf
  3. the asset name
  4. each taxonomy node's suffix, also root to leaf (same traversal as the prefixes)
  5. the variant (unless it is Base), then lod, then number values
  6. the asset type's suffix

The engine folder path is derived separately: walk the same path and join the names of every node whose folder is true with /. Node prefixes and suffixes never affect the folder — only the node names do.

Finally, the assembled name is checked against the asset type's naming.pattern and naming.max_length — that's what the naming.convention validator enforces at export time.

Source paths

By default (mirrors_engine), the working file for an asset lives at the same relative path as its engine folder — a mesh headed for Props/Weapons keeps its .blend/.ma under Props/Weapons in the source root. To place sources elsewhere, give the asset type a template_override:

YAMLsource_path:
  template_override: "Art/Source/{{ category }}/{{ subcategory }}"

Templates use Jinja-style double-brace syntax. Available variables:

VariableValue
categoryFirst taxonomy segment (e.g. Props).
subcategorySecond segment (e.g. Weapons).
subcategory2, subcategory3, …Deeper segments, numbered from the third.
variant, lod, number, …The dimension values selected for the asset (any selection key the DCC sends).

This is the only place templating exists in v3. Legacy single-brace { category } syntax from older documents is migrated to {{ category }} automatically on load. The asset's name is not a template variable — names come from the structural assembly above.

Authoring in Lua

When rules are computed rather than written out — dozens of asset types sharing patterns, prefixes derived from a table, per-category poly budgets from a formula — you can author the document as a Lua script instead of YAML. Switch the editor's mode selector to Lua Script (or save via the API with ?type=lua).

The contract: define a config() function that returns a table with exactly the same v3 structure as the YAML. The server runs it at save and compile time — clients always receive the same compiled JSON regardless of authoring format.

LUA-- Generate several asset types from one table.
local types = {
  { key = "static_mesh",   prefix = "SM" },
  { key = "skeletal_mesh", prefix = "SK" },
  { key = "texture",       prefix = "T"  },
  { key = "animation",     prefix = "A"  },
}

function config()
  local asset_types = {}
  for _, t in ipairs(types) do
    asset_types[t.key] = {
      prefix = t.prefix,
      naming = { pattern = "^" .. t.prefix .. "_[A-Za-z0-9_]+$" },
      categories = { Characters = {}, Environment = {}, Props = {} },
    }
  end
  return {
    version = 3,
    dimensions = { number = { required = true, values = {} } },
    validators = {
      { id = "naming.convention", severity = "error" },
    },
    asset_types = asset_types,
  }
end

The script runs in a sandbox: only the table, string, and math libraries are available — no os, io, require, load, or debug — with a 1 MB memory ceiling and an instruction limit, so a typo'd infinite loop errors out instead of hanging the server. Compile errors, a missing config(), or a return value that isn't a valid v3 table are all rejected at save, with the exact error shown in the editor.

In Lua mode the portal editor adds a Test Script panel: give it an asset name, an asset type, and a context (taxonomy path plus dimension values as JSON) and it dry-runs the script, returning the resolved name, engine path, and source path — without saving anything.

GET /api/projects/:id/conventions returns whatever you authored — the Lua source verbatim for a Lua document, YAML otherwise. The compiled JSON that clients use is always available from /conventions/compiled, whichever format you author in.

The portal editor

What is checked when

MomentWhat can fail
Saving YAMLInvalid YAML; invalid template syntax in template_override.
Saving LuaLua compile errors; missing config(); a return value that doesn't compile as v3 (including bad regexes) — Lua saves are fully compile-checked.
Compiling (fetch)Wrong/missing version; invalid naming.pattern regex; subcategories at asset-type level; malformed validator entries.
NeverUnknown validator ids (clients skip rules they don't implement); dimension values outside values (advisory).

A bad regex in a YAML document isn't caught until compile — the plain YAML save only checks syntax and templates. Use the Quick Toggle tree or the Validate Names panel right after saving to confirm the document compiles, or author in Lua, where saves are compile-checked end to end.

API reference

EndpointPurpose
GET /api/projects/:id/conventionsThe authored document, verbatim — YAML or Lua source.
PUT /api/projects/:id/conventionsReplace the document. Admin only. ?type=template (YAML, the default) or ?type=lua; omitting type keeps the stored format.
GET /api/projects/:id/conventions/compiledThe compiled JSON clients consume — taxonomy resolved, effective_validators baked onto every asset type and node.
GET /api/projects/:id/conventions/jsonAlias of /compiled.

Clients cache the compiled document for 5 minutes. After publishing a change, expect a short delay before every DCC picks it up; the Unreal plugin refreshes on its own timer as well.