editor.inventory
Thirteen methods covering multi-source (Fab / GameWalk / USD / Manual) scan, multi-project reporting, and cross-project asset migration. Shipped in the Asset Inventory Redesign (AIR) milestone. AI agents reach through these methods to answer "where is this asset used" and "move this asset plus its dependency cascade into another project" without scripting Content Browser UI.
Backed by SQLite + v2 schema. Each call opens the local catalog.db at {HomeProject}/FabCatalog/catalog.db, performs its work, and closes the handle. Writes are atomic per call. Two-sided read / read-write coexistence with the Slate Asset Inventory panel is handled by the AIR-FIX-5 auto-close contract; see the Concurrency section below.
Method summary
| Method | Mutating | Tier | Purpose |
|---|---|---|---|
scanProject | yes | Free | Kick off an async provider scan; returns a scan_id immediately. |
getScanStatus | no | Free | Poll an in-flight or recently-terminal scan by scan_id. |
cancelScan | yes | Free | Cooperatively cancel an in-flight scan by scan_id. |
listProjects | no | Free | Enumerate every project observed in the inventory. |
listAssets | no | Free | Browse inventory assets, optional filters on project_path + source. |
findUsages | no | Free | Cross-project memberships for one asset -- which projects use it. |
getUsageReport | no | Free | Assets ranked by project_count DESC. |
syncToNeo4j | yes | Pro | Bulk UNWIND/MERGE the v2 inventory into Neo4j as disjoint :Inventory* labels. |
migrateAsset | yes | Free | Copy an asset + its hard-package dependency cascade into another project. |
pruneProjectKeys | yes | Free | Remove membership rows whose project_path no longer resolves on disk. |
deleteProject | yes | Free | Intentionally retire a project's memberships. |
canonicalizeProjectKeys | yes | Free | One-shot rekey of non-canonical membership rows; merge duplicates. |
deleteAssets | yes | Free | Bulk delete by unreal_paths; optional project_path scopes to membership-only removal. |
Twelve methods are Free. syncToNeo4j is the sole tier-gated method in the namespace (Pro) because the customer-side Neo4j is a paid-tier resource. See pricing for tier details.
Project-path canonicalization
Methods that accept a project_path canonicalize the value before touching the database. Equivalent inputs collapse to the same key so G:/Home57t, g:\Home57t\, G:/Home57t/, and G:\Home57t all map to the single row G:/Home57t. Rules:
- Trim surrounding whitespace
- Resolve to absolute path via
FPaths::ConvertRelativePathToFull - Backslashes become forward slashes
- Trailing slash stripped unless the path is a root
- Drive letter uppercased on Windows
Writes always use the canonical form. scanProject additionally validates that a supplied project_path matches the editor's currently-running project; a mismatch is rejected with -32602 ProjectPathMismatch and no DB changes occur. Typo'd paths cannot create phantom project rows in listProjects.
editor.inventory.scanProject
Start an asynchronous multi-provider scan against the running editor's project. Returns a scan_id within about a second regardless of project size. The scan runs on a TaskGraph worker; clients poll getScanStatus for progress and terminal result. Cooperative cancel via cancelScan.
Idempotent while running: a second scanProject call for the same canonicalized project_path returns the existing scan_id with reused: true instead of starting a second worker. After termination, a subsequent call allocates a fresh scan_id.
Save first. The GameWalk provider reads the Asset Registry's on-disk index. Assets that exist only in memory (newly created or edited but never saved) are not captured by the scan. Save outstanding editor work before invoking this method. Fab and USD providers are unaffected.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
project_path | string | No | Canonical path of the UE project. Omit to auto-derive from FPaths::ProjectDir(). If supplied, must equal the editor's running project; mismatch returns -32602 ProjectPathMismatch. |
source_override | string | No | One of fab | usd | gamewalk | manual. Re-tags every discovered asset with this source before DB upsert. Intended for baseline imports of Fab-seeded projects. |
Returns:
| Name | Type | Description |
|---|---|---|
scan_id | string (GUID) | UUID for subsequent getScanStatus / cancelScan calls. |
started_at | string (ISO 8601) | Timestamp of scan registration. |
reused | bool | true if an existing in-flight scan was returned; false if a new worker was started. |
Example Request:
{
"jsonrpc": "2.0", "id": 1,
"method": "editor.inventory.scanProject",
"params": { "source_override": "fab" }
} Example Response:
{
"jsonrpc": "2.0", "id": 1,
"result": {
"scan_id": "8e0c1c1d-8d45-4e12-9c0e-6a3f2b5c1a42",
"started_at": "2026-04-15T17:42:01.432Z",
"reused": false
}
} editor.inventory.getScanStatus
Read-only poll by scan_id. Scan records are retained for about 60 seconds after terminal state; a 15-second-cadence reaper evicts entries past that window. Polling clients that read slower than 60 seconds miss the terminal state and must re-scan.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
scan_id | string (GUID) | Yes | UUID returned by scanProject. Invalid GUID returns -32602. Unknown or evicted scan_id returns -32001 ScanNotFound. |
Returns:
| Name | Type | Description |
|---|---|---|
scan_id | string | Echo of the GUID. |
state | string | One of pending, running, complete, cancelled, failed. |
started_at | string (ISO 8601) | Timestamp of scanProject invocation. |
progress | object | {stage, assets_discovered, assets_upserted, elapsed_ms}. Stage is one of starting, enumerating, persisting, pruning, finished, cancelled. |
terminal_at | string (ISO 8601) | Present on terminal states. Timestamp of transition. |
result | object | Present when state = complete. Includes project_path, duration_ms, providers_run, assets_upserted, collections_upserted, memberships_added, memberships_pruned, cancelled, provider_errors. |
error | object | Present when state = failed. {code, message} surfacing the engine error. |
Example Request:
{
"jsonrpc": "2.0", "id": 2,
"method": "editor.inventory.getScanStatus",
"params": { "scan_id": "8e0c1c1d-8d45-4e12-9c0e-6a3f2b5c1a42" }
} Example Response (running):
{
"jsonrpc": "2.0", "id": 2,
"result": {
"scan_id": "8e0c1c1d-8d45-4e12-9c0e-6a3f2b5c1a42",
"state": "running",
"started_at": "2026-04-15T17:42:01.432Z",
"progress": { "stage": "persisting", "assets_discovered": 2041, "assets_upserted": 1987, "elapsed_ms": 8912 }
}
} editor.inventory.cancelScan
Sets the engine's cooperative cancel flag for a running scan. Idempotent: calling on an already-terminal scan returns {cancel_requested: false, state: <current>} without touching the engine. The scan transitions to state = cancelled within about two seconds. Partial rows already persisted are retained; the prune pass is skipped on cancel.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
scan_id | string (GUID) | Yes | UUID returned by scanProject. |
Returns:
| Name | Type | Description |
|---|---|---|
scan_id | string | Echo of the GUID. |
cancel_requested | bool | true if the handler set the engine cancel flag; false if the scan was already terminal. |
state | string | Current registry state. Typically still running immediately after cancel; poll getScanStatus to observe the transition to cancelled. |
Example Request:
{
"jsonrpc": "2.0", "id": 3,
"method": "editor.inventory.cancelScan",
"params": { "scan_id": "8e0c1c1d-8d45-4e12-9c0e-6a3f2b5c1a42" }
} Example Response:
{
"jsonrpc": "2.0", "id": 3,
"result": {
"scan_id": "8e0c1c1d-8d45-4e12-9c0e-6a3f2b5c1a42",
"cancel_requested": true,
"state": "running"
}
} editor.inventory.listProjects
Enumerate every project observed in the inventory, ordered by last_scanned DESC. Empty inventory returns {"projects": []} with no error.
No parameters.
Returns:
| Name | Type | Description |
|---|---|---|
projects | array | One object per project: {project_path, asset_count, last_scanned}. |
Example Request:
{ "jsonrpc": "2.0", "id": 2, "method": "editor.inventory.listProjects" } Example Response:
{
"jsonrpc": "2.0", "id": 2,
"result": {
"projects": [
{ "project_path": "D:/UnrealProjects/Demo574c", "asset_count": 412, "last_scanned": "2026-04-13T17:42:18Z" },
{ "project_path": "E:/UnrealProjects/Home57t", "asset_count": 88, "last_scanned": "2026-04-12T20:14:09Z" }
]
}
} editor.inventory.listAssets
Browse inventory assets with optional filters. Omit project_path for cross-project listing. Omit source to include all sources. Unknown project_path returns {"assets": [], "total": 0} (no error).
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
project_path | string | No | Canonicalized at read-time. Omit for cross-project listing. |
source | string | No | One of fab | usd | gamewalk | manual. |
limit | number | No | Default 100. |
offset | number | No | Default 0. |
Returns:
| Name | Type | Description |
|---|---|---|
assets | array | Per-asset rows including unreal_path, source, class_name, disk_size_bytes, and class-specific metadata (triangle_count / vertex_count / lod_count / nanite_enabled for StaticMesh, dimensions / sRGB for Texture2D). |
total | number | Total rows matching the filter. |
Example Request:
{
"jsonrpc": "2.0", "id": 3,
"method": "editor.inventory.listAssets",
"params": { "project_path": "D:/UnrealProjects/Demo574c", "source": "fab", "limit": 50, "offset": 0 }
} Example Response:
{
"jsonrpc": "2.0", "id": 3,
"result": {
"assets": [
{
"unreal_path": "/Game/MyAssets/RusticChair.RusticChair",
"source": "fab",
"collection_id": "fab:asset_xyz123",
"name": "RusticChair",
"class_name": "StaticMesh",
"disk_size_bytes": 4187423,
"triangle_count": 18432,
"vertex_count": 9241,
"lod_count": 4,
"nanite_enabled": true,
"first_seen_at": "2026-04-09T12:00:00Z",
"last_seen_at": "2026-04-13T17:42:18Z"
}
],
"total": 1
}
} editor.inventory.findUsages
Return every project that uses a given asset. Unknown unreal_path returns {"projects": [], "usage_count": 0} (no error). Missing or empty unreal_path returns JSON-RPC error -32602.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
unreal_path | string | Yes | Full UObject path (e.g. /Game/Shared/CommonMaterial.CommonMaterial). |
Returns:
| Name | Type | Description |
|---|---|---|
unreal_path | string | Echo of the queried path. |
projects | array | Per-project rows: {project_path, first_seen, last_seen}. |
usage_count | number | Number of projects the asset belongs to. |
Example Request:
{
"jsonrpc": "2.0", "id": 4,
"method": "editor.inventory.findUsages",
"params": { "unreal_path": "/Game/Shared/CommonMaterial.CommonMaterial" }
} Example Response:
{
"jsonrpc": "2.0", "id": 4,
"result": {
"unreal_path": "/Game/Shared/CommonMaterial.CommonMaterial",
"projects": [
{ "project_path": "D:/UnrealProjects/Demo574c", "first_seen": "2026-04-09T12:00:00Z", "last_seen": "2026-04-13T17:42:18Z" },
{ "project_path": "E:/UnrealProjects/Home57t", "first_seen": "2026-04-10T14:23:00Z", "last_seen": "2026-04-12T20:14:09Z" }
],
"usage_count": 2
}
} editor.inventory.getUsageReport
Rank every asset by how many projects it appears in. Inverse lens to findUsages: given the catalog, which assets are the most reused? Powers the Slate "Asset Usage Report" tab and the agentux_inventory_get_usage_report MCP tool.
Rows are ordered by project_count DESC with unreal_path ASC as a stable tiebreaker. next_offset is offset + items.length when the page is full, and null on the terminal page.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
limit | number | No | Default 100. Range [1, 1000]. Invalid returns -32602. |
offset | number | No | Default 0. Must be non-negative. |
min_project_count | number | No | Default 2. Pass 1 to include single-project assets. |
Returns:
| Name | Type | Description |
|---|---|---|
items | array | Per-asset rows: {unreal_path, name, source, class_name, project_count}. |
next_offset | number or null | offset + items.length on full page; null on terminal page. |
Example Request:
{
"jsonrpc": "2.0", "id": 6,
"method": "editor.inventory.getUsageReport",
"params": { "limit": 20, "offset": 0, "min_project_count": 2 }
} Example Response:
{
"jsonrpc": "2.0", "id": 6,
"result": {
"items": [
{ "unreal_path": "/Game/Shared/CommonMaterial.CommonMaterial", "name": "CommonMaterial", "source": "fab", "class_name": "Material", "project_count": 5 },
{ "unreal_path": "/Game/Shared/GreyConcrete.GreyConcrete", "name": "GreyConcrete", "source": "fab", "class_name": "Material", "project_count": 3 },
{ "unreal_path": "/Game/Shared/Plank01.Plank01", "name": "Plank01", "source": "fab", "class_name": "StaticMesh", "project_count": 2 }
],
"next_offset": null
}
} editor.inventory.syncToNeo4j Pro
Pro-tier bulk sync of the v2 SQLite inventory into Neo4j. Idempotent: re-running against an already-synced graph updates scalar properties and lastSeenAt timestamps without creating duplicate nodes or edges. Disjoint from the :GraphRAG label namespace by construction; disjoint from the retired v1 :FabAsset / :FabPack graph by separate labels.
Free callers receive -32007 from the router's pre-dispatch tier check. Neo4j is required and reachable at the configured UAgentUXSettings::Neo4jHost:7474. Credentials come from FAgentUXCredentialStore (OS keyring, key graphrag-neo4j) with NEO4J_PASSWORD as an environment-variable fallback.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
batch_size | number | No | Default 500. Range [1, 5000]. |
dry_run | bool | No | Default false. Skips Neo4j writes; returns projected counters for preview. |
Returns:
| Name | Type | Description |
|---|---|---|
assets_synced | number | Input rows processed for the asset pass. |
projects_synced | number | Input rows for the project pass. |
memberships_synced | number | Input rows for the membership pass. |
collections_synced | number | Input rows for the collection pass. |
errors | number | Count of batch failures across all passes. |
error_message | string | Empty on clean runs; non-empty on status = partial. |
status | string | complete when errors == 0; otherwise partial. |
dry_run | bool | Echo of the input. |
Example Request:
{
"jsonrpc": "2.0", "id": 7,
"method": "editor.inventory.syncToNeo4j",
"params": { "batch_size": 500, "dry_run": false }
} Example Response:
{
"jsonrpc": "2.0", "id": 7,
"result": {
"assets_synced": 1673,
"projects_synced": 3,
"memberships_synced": 1820,
"collections_synced": 12,
"errors": 0,
"error_message": "",
"status": "complete",
"dry_run": false
}
} editor.inventory.migrateAsset
Copy an asset and its hard-package dependency cascade into another project. Mode is copy only. Cascade defaults to true and walks /Game/-scoped hard-package references via the Asset Registry. Conflict policy skips targets that already exist (no overwrite).
source_project must equal the project the editor is currently running. The handler resolves source disk paths via FPackageName::DoesPackageExist against the live mount table; cross-project source resolution is out of scope.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
source_unreal_path | string | Yes | Full UObject path to migrate. |
source_project | string | Yes | Must equal the editor's running project. |
target_project | string | Yes | Destination project root. |
cascade | bool | No | Default true. Walks hard-package dependencies under /Game/. |
Returns:
| Name | Type | Description |
|---|---|---|
migrated | array | Packages successfully copied. |
skipped | array | Rows carry a parenthesized reason (e.g. "/Game/Foo (source not found on disk)"). |
conflicts | array | Target packages that already exist; skip-on-conflict leaves them untouched. |
dependency_cascade | array | Full list of packages the walker considered. |
Example Request:
{
"jsonrpc": "2.0", "id": 5,
"method": "editor.inventory.migrateAsset",
"params": {
"source_unreal_path": "/Game/Shared/CommonMaterial.CommonMaterial",
"source_project": "D:/UnrealProjects/Demo574c",
"target_project": "D:/UnrealProjects/NewProject",
"cascade": true
}
} Example Response:
{
"jsonrpc": "2.0", "id": 5,
"result": {
"source_unreal_path": "/Game/Shared/CommonMaterial.CommonMaterial",
"target_project": "D:/UnrealProjects/NewProject",
"cascade": true,
"migrated": ["/Game/Shared/CommonMaterial", "/Game/Shared/Textures/Wood_BC", "/Game/Shared/Textures/Wood_N"],
"skipped": [],
"conflicts": [],
"dependency_cascade": ["/Game/Shared/CommonMaterial", "/Game/Shared/Textures/Wood_BC", "/Game/Shared/Textures/Wood_N"]
}
} editor.inventory.pruneProjectKeys
Maintenance method. Remove asset_project_membership rows whose project_path does not resolve to a directory that currently exists on disk. Typo'd inputs (Home57t, G:/Home57t on a machine with no G: drive) create phantom project rows; this method cleans them out. Asset rows in asset_v2 are preserved.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
dry_run | bool | No | Default false. Report what would be pruned without deleting. |
Returns:
| Name | Type | Description |
|---|---|---|
dry_run | bool | Echo of the input. |
pruned_keys | number | Count of project keys removed (or that would be removed on dry-run). |
removed_memberships | number | Membership rows removed; 0 on dry-run. |
pruned | array | Per-key audit rows. |
kept | array | Keys not removed (directory exists on disk). |
Example Request:
{
"jsonrpc": "2.0", "id": 5,
"method": "editor.inventory.pruneProjectKeys",
"params": { "dry_run": true }
} Example Response:
{
"jsonrpc": "2.0", "id": 5,
"result": {
"dry_run": true,
"pruned_keys": 2,
"removed_memberships": 0,
"pruned": [
{ "project_path": "Home57t", "canonical_project_path": "G:/some/cwd/Home57t", "asset_membership_count": 12, "would_delete": true, "deleted": 0 }
],
"kept": [
{ "project_path": "E:/UnrealProjects/Home56t", "canonical_project_path": "E:/UnrealProjects/Home56t", "asset_membership_count": 88, "would_delete": false }
]
}
} editor.inventory.deleteProject
Intentional-retirement counterpart to pruneProjectKeys. While pruneProjectKeys only sweeps rows whose project_path no longer resolves to a real directory, deleteProject removes a project's memberships regardless of whether the directory still exists. Use it when a project is retired and should drop out of cross-project reporting. Asset rows in asset_v2 are preserved. Deletes are terminal -- no undo.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
project_path | string | Yes | Canonicalized at handler entry; equivalent inputs collapse to one key. |
Returns:
| Name | Type | Description |
|---|---|---|
project_path | string | Echo of the input. |
canonical_project_path | string | Canonical form after normalization. |
removed_memberships | number | Membership rows deleted. |
Example Request:
{
"jsonrpc": "2.0", "id": 7,
"method": "editor.inventory.deleteProject",
"params": { "project_path": "E:/UnrealProjects/RetiredProject" }
} Example Response:
{
"jsonrpc": "2.0", "id": 7,
"result": {
"project_path": "E:/UnrealProjects/RetiredProject",
"canonical_project_path": "E:/UnrealProjects/RetiredProject",
"removed_memberships": 412
}
} editor.inventory.canonicalizeProjectKeys
One-shot maintenance. Rewrite every membership row's project_path to canonical form and merge duplicates produced by the rekeying. Use when the database contains pre-AIR-FIX-2 rows whose project path uses backslashes, lowercase drive letters, or trailing slashes -- variants that pruneProjectKeys cannot collapse because both forms resolve to a real directory.
Collisions merge via MIN(first_seen_at) and MAX(last_seen_at). The whole pass runs inside BEGIN IMMEDIATE / COMMIT; a partial failure rolls back rather than leaving the table half-rekeyed.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
dry_run | bool | No | Default false. Report renames without mutating. |
Returns:
| Name | Type | Description |
|---|---|---|
dry_run | bool | Echo of the input. |
renamed_rows | number | Rows rekeyed without collision. |
merged_rows | number | Rows folded into a pre-existing canonical row via MIN/MAX coalesce. |
renames | array | Per-key {from, to} pairs. |
Example Request:
{
"jsonrpc": "2.0", "id": 8,
"method": "editor.inventory.canonicalizeProjectKeys"
} Example Response:
{
"jsonrpc": "2.0", "id": 8,
"result": {
"dry_run": false,
"renamed_rows": 7,
"merged_rows": 2,
"renames": [
{ "from": "e:\\UnrealProjects\\Home57t", "to": "E:/UnrealProjects/Home57t" },
{ "from": "D:/UnrealProjects/Demo574c/", "to": "D:/UnrealProjects/Demo574c" }
]
}
} editor.inventory.deleteAssets
Bulk delete by list of unreal_paths. Two modes:
- Project-scoped (
project_pathsupplied): removes only the(unreal_path, project_path)membership rows.asset_v2rows are preserved. - Unscoped (
project_pathomitted): deletes theasset_v2rows themselves; FKON DELETE CASCADEremoves all memberships for those paths across every project.
Cap: 1000 paths per call (-32602 if exceeded). Batch on the caller side for larger sets. Deletes are terminal -- no undo. The destructive surface is JSON-RPC + CLI only; there is no Slate UI. Use the inventory_cleanup.py search-first / purge-second flow for safety.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
unreal_paths | array<string> | Yes | 1 to 1000 full UObject paths. |
project_path | string | No | Canonicalized. Omit for unscoped delete. |
Returns:
| Name | Type | Description |
|---|---|---|
removed_assets_count | number | Non-zero only in unscoped mode. |
removed_memberships_count | number | Membership rows removed. |
unreal_paths | array | Echo of the input. |
project_path | string | Canonical form if supplied; empty string if unscoped. |
Example Request:
{
"jsonrpc": "2.0", "id": 10,
"method": "editor.inventory.deleteAssets",
"params": {
"unreal_paths": ["/Game/MyAssets/SM_Test1.SM_Test1", "/Game/MyAssets/SM_Test2.SM_Test2"],
"project_path": "E:/UnrealProjects/Home57t"
}
} Example Response:
{
"jsonrpc": "2.0", "id": 10,
"result": {
"removed_assets_count": 0,
"removed_memberships_count": 2,
"unreal_paths": ["/Game/MyAssets/SM_Test1.SM_Test1", "/Game/MyAssets/SM_Test2.SM_Test2"],
"project_path": "E:/UnrealProjects/Home57t"
}
} Concurrency note
UE's embedded SQLite VFS on Windows cannot host two read-write opens in-process, and also fails read-write alongside a pre-existing read-only handle. Two sides of the same defect; each is handled differently.
Scan-then-open-panel (auto-fixed). Every editor.inventory.* method opens catalog.db, does its work, and releases the handle before returning. The Slate Asset Inventory panel's read-only open during tab activation succeeds cleanly even immediately after an RPC. No customer workaround required.
Destructive RPC while the panel is already open (rule still applies). If the Asset Inventory panel is open (holding its read-only handle), running a destructive RPC -- scanProject, migrateAsset, pruneProjectKeys, deleteProject, canonicalizeProjectKeys, deleteAssets -- from CLI or MCP fails with -32003 Failed to open catalog database. Close the Asset Inventory tab (Window → AgentUX → Asset Inventory → X) before running destructive CLIs like inventory_cleanup.py, then reopen after the call returns.
MCP tool mirrors
The MCP bridge ships nine @mcp.tool() wrappers corresponding to these methods. agentux_inventory_scan_project hides the scan_id poll loop internally, so Claude callers see one-call-one-result ergonomics. The four destructive maintenance methods (pruneProjectKeys, deleteProject, canonicalizeProjectKeys, deleteAssets) are intentionally JSON-RPC + CLI only; MCP exposure is reserved for the inventory_cleanup.py search-first flow and direct agentux_raw use.
Related
- Asset Inventory product page -- feature overview and screenshots
- editor.fab / editor.fab.catalog -- Fab library + legacy v1 catalog, including retired methods
- Pricing -- tier comparison including Pro-tier Neo4j access