HELIX Docs
Platform API

Interactive Universal Items

Production implementation contract for portable items with authoritative behavior, typed state, character abilities, and default-on Vault publication.

Interactive Universal Items

An Interactive Universal Item is a portable, ownable object whose definition includes its assets, behavior, interaction surface, and state schema. A shower, pinball machine, weapon, vehicle, or appliance is published once, works in many worlds, and keeps the same public functions and events while each owned copy and world placement keeps its own state.

Implementation handoff — target contract

The product/storage seams below are live, and the prototype proves the interaction model. The canonical package, production item runtime, SDK surface, and Web/Unreal adapters described after the status table are the contract to implement. They are not shipped APIs yet.

For the deployed upload/finalize/read behavior, use the live wearable and Unreal contract. To run the same dual-format probe against real cooked .pak, .utoc, and .ucas package outputs, send the Unreal wearable test to the integration engineer.

Current live surface versus target

SurfaceCurrent live or provenProduction work still required
Product and ownershipuniversal_items is the product; every user_inventory_items row is an owned instance with JSON state.Validate owned state against the item definition and add revisioned writes.
PlacementHome placements already store an inventory-instance link, transform, placement state, and metadata.Apply the same placement overlay contract to every World/Instance runtime.
VaultVault assets have durable IDs, immutable numeric versions, compatibility, typed metadata, and usage tracking. A Universal Item can reference one through vault_asset_id.Publish the item definition and dependency-complete package through this path for every reusable item.
Default publicationusers.vault_auto_publish_generated_assets is true by default and generation respects the preference.Make Universal Item finalization use the same default-on policy and accept an explicit per-publish opt-out.
Character systemThe humanoid runtime has abilities, channel arbitration, named sockets, clip registration, and layered animation.Ship a first-party ItemInteractionAbility and permit safe runtime clip/layer contribution.
Interactive behaviorThe prototype proves typed vars, functions/events, sockets, declarative authority, replication filtering, capabilities, takeover input, cues, sandboxing, and call budgets.Move the contract into a versioned shared schema and run it in the production Instance authority.
Runtime parityThe prototype renders on the Web path.Web and Unreal must consume one manifest and one authoritative state protocol; only presentation adapters differ.

The production boundary is deliberate: do not expose Helix.items publicly until the conformance plan at the end passes on both runtimes.

Non-negotiable model

Keep these layers separate:

  1. Definition — immutable manifest, state schema/defaults, functions, events, sockets, logic, compatibility, and pinned Vault dependencies.
  2. Owned instance — ownership plus portable persistent state, keyed by inventory instance ID.
  3. Placement — world-local transform, approved overrides, bindings, and placement state.
  4. Session — authoritative live state replicated to players in the current Instance.
  5. Private authority state — secrets and anti-cheat data that are never sent to clients.

Definitions reference durable Vault asset IDs and pinned versions. They never persist CDN URLs. Installation creates fresh owned/placement IDs and state from defaults; it never copies another owner's state.

Canonical package

The package extension is .hlxi. Version 1 remains a deterministic container, but production packages contain a small manifest/logic graph and pinned Vault references rather than duplicating large media blobs.

item.hlxi
├── item.manifest.json
├── logic/authority.hlx.json       # engine-neutral, executed by Instance authority
├── logic/presentation.web.mjs     # optional, untrusted Web presentation
├── logic/presentation.unreal.json # optional, declarative Unreal presentation mapping
├── migrations/1-to-2.hlx.json     # deterministic state migration, when needed
└── checksums.json                 # SHA-256 for every packaged file

The canonical identity has three independent versions:

  • schemaVersion — item-manifest contract version, beginning at 1.0.
  • definition.version — semantic version of the item behavior and public surface.
  • Vault dependency version — immutable integer version of each referenced Vault asset.

The published bundle and every dependency are content-addressed. A world may follow the latest definition for authoring, but a published World Build pins exact item and asset versions.

Manifest v1

This is the normative shape. JSON Schema should be generated from these types and published at /schemas/interactive-item-manifest/1.0.

type JsonSchema = Record<string, unknown>;
type StatePartition = 'owned' | 'placement' | 'session' | 'private';
type Replication = 'none' | 'owner' | 'instance';
type ItemAuthority = 'platform' | 'instance';
type ItemAuth = 'anyone' | 'owner' | 'world-admin' | 'linked-item' | 'authority';

type VaultRef = {
  assetId: string;                 // vault_assets.id UUID; never a URL
  version: number;                 // immutable vault_asset_versions.version
  kind:
    | 'prop' | 'character' | 'animation' | 'audio' | 'texture' | 'material'
    | 'environment' | 'terrain' | 'vfx' | 'decal' | 'sky'
    | 'gaussian_splat' | 'scene' | 'unreal_package';
  role: string;                    // model.primary, animation.reload, audio.fire, ...
  required: boolean;
};

type ItemStateField = {
  schema: JsonSchema;
  default: unknown;
  authority: ItemAuthority;
  persistence: 'portable' | 'placement' | 'transient' | 'private';
  replication: Replication;
  writableBy: 'authority' | 'owner' | 'world-admin';
};

type ItemFunction = {
  input: JsonSchema;
  output?: JsonSchema;
  auth: ItemAuth;
  mutates: StatePartition[];
  idempotency: 'required' | 'optional' | 'forbidden';
  rateLimit?: { calls: number; perSeconds: number };
  doc: string;
};

type ItemEvent = {
  payload: JsonSchema;
  delivery: 'reliable' | 'ephemeral';
  audience: Replication;
};

type InteractiveItemManifestV1 = {
  schemaVersion: '1.0';
  definition: {
    id: `helix:item:${string}`;
    version: `${number}.${number}.${number}`;
    title: string;
    description: string;
    kind: 'home_item' | 'prop' | 'wearable' | 'weapon' | 'vehicle' | 'appliance';
    implements: string[];          // e.g. interactive_item, weapon.ranged
  };
  compatibility: {
    engines: Array<'web' | 'unreal'>;
    systems: string[];             // e.g. humanoid-character@^0.2.8
    skeleton?: 'helix-humanoid@1';
  };
  assets: Record<string, VaultRef>;
  physical: {
    sizeMeters: [number, number, number];
    pivot: 'footprint-center';
    collision: { mode: 'bounds' | 'vault-ref' | 'none'; asset?: string };
  };
  sockets: Record<string, {
    type: 'stand' | 'seat' | 'camera' | 'hand' | 'surface' | 'slot' | 'effect';
    position: [number, number, number];
    rotation?: [number, number, number];
    node?: string;
  }>;
  state: Record<StatePartition, Record<string, ItemStateField>>;
  functions: Record<string, ItemFunction>;
  events: Record<string, ItemEvent>;
  cues: Record<string, {
    itemAnimation?: string;
    actorAnimation?: string;
    audio?: string;
    vfx?: string;
    at?: string;
    loop?: boolean;
  }>;
  character?: {
    ability: 'item-interaction';
    channels: Record<string, { priority: number; mode: 'exclusive' | 'shared' }>;
    input: Partial<Record<'move' | 'look' | 'primary' | 'secondary' | 'tertiary' | 'cancel', string>>;
  };
  capabilities: string[];
  logic: {
    authoritative: { format: 'helix-declarative-v1'; entry: string };
    presentation?: Partial<Record<'web' | 'unreal', { format: string; entry: string }>>;
  };
  authoring: {
    portableOverridePaths: string[];
    worldBindings: Array<{ event: string; target: string; function: string }>;
  };
  publication: {
    vault: 'default' | 'never';    // default obeys the creator preference; never is explicit opt-out
    primaryAsset: string;          // key in assets
  };
  migrations?: Array<{ from: string; to: string; entry: string }>;
};

An interactive item is not a new Vault media kind. Its primary render resource retains its real kind—usually prop—and is searchable with implements: ["interactive_item", ...]. The item package is attached as the target definition.item_manifest related artifact. Implementing that role requires widening the current material-only related-artifact gate. This preserves the distinction between a reusable resource and an ownable product while making interactive behavior discoverable.

Vault dependency contracts

Item dependencies use the settled typed Vault formats:

KindCanonical descriptor or artifact
material.helix-material.jsonapplication/vnd.helix.material+json; maps use source.albedo, source.normal, source.roughness, and optional runtime.ktx2.albedo, runtime.ktx2.normal, runtime.ktx2.roughness roles
scene.helix-scene.jsonapplication/vnd.helix.scene+json; v1 nodes reference a Vault asset UUID plus position/rotation/scale, with optional runtime bundles
sky.helix-sky.jsonapplication/vnd.helix.sky+json; pins HDR/EXR source and generated lighting/backdrop renditions
terrain.helix-terrain.jsonapplication/vnd.helix.terrain+json
vfx.helix-vfx.jsonapplication/vnd.helix.vfx+json; adapters map the portable graph to Web effects or Niagara
gaussian_splat.spzmodel/vnd.gaussian-splat.spz; gaussianSplat.scope is object, fragment, or environment

The current related-artifact input is Array<{ role, artifact: { filename, buffer, contentType? } }> and the resolved read shape is { role, url, mimeType, sizeBytes, checksumSha256 }. Immutable version metadata stores { role, storageKey, storageTarget, mimeType, sizeBytes, checksumSha256 }. Materials currently identify as pbr-material-preview, report preview-beta PBR/tileability capability, and declare either portable-source-only or portable-source-plus-ktx2.

Every final-mesh item thumbnail also carries metadata.canonicalThumbnail: source validated-final-mesh, renderer version final-mesh-textured-v1, transparent background, black UI composite, camera preset, and source SHA-256. Provider/reference images remain provenance-only and must never become the canonical item thumbnail.

State, revision, and idempotency

PartitionStored onAuthorityPersistenceReplicationPublish behavior
ownedinventory instanceplatform/Instance command serviceTravels with the owned copyOwner or Instance, field-by-fieldValues reset to manifest defaults
placementworld placementInstance authorityStays with that placementInstance, field-by-fieldValues removed; allowlisted authoring overrides may remain
sessionlive InstanceInstance authorityNever persistedInstance or ownerAlways removed
privateauthority storeplatform/Instance authorityAs explicitly requiredNeverAlways removed, including defaults marked secret

Every state partition has an integer revision. Every mutation carries expected revisions and returns the revisions it committed. A conflict returns RevisionConflict; clients refetch and retry from the new snapshot instead of overwriting concurrent changes.

Every function call carries a caller-generated idempotencyKey. The authority stores the terminal result for functions marked required; the same caller/key/function/instance tuple returns the recorded result without running twice. This is mandatory for durability, inventory, currency, rewards, score finalization, ammunition consumption, and ownership effects.

type ItemCallRequest<T> = {
  instanceId: string;
  function: string;
  args: T;
  idempotencyKey: string;
  expectedRevisions?: Partial<Record<StatePartition, number>>;
};

type ItemCallResult<T> = {
  callId: string;
  value: T;
  revisions: Record<StatePartition, number>;
  emitted: Array<{ sequence: number; event: string; payload: unknown }>;
};

Hydration order is fixed: definition defaults → owned state → placement state/overrides → session defaults → onSpawn. No client renders a default state and then replaces it with persistent state.

Lifecycle

The authoritative runtime owns this sequence:

validate → install → hydrate → place → spawn → activate
                                  ↕ calls / input / events / ticks
                              deactivate → despawn → unplace
transfer: flush owned state → revoke old authority → attach new owner
upgrade: snapshot → deterministic migration → validate → commit atomically

Only onSpawn, onCall, onInput, onTick, and onDespawn execute item logic. install, place, transfer, publication, and migration are platform commands. Logic cannot forge ownership or bypass them.

SDK and runtime surface

The public Web API is asynchronous and authority-backed:

interface HelixItems {
  inspect(instanceId: string): Promise<ItemSnapshot>;
  getActions(instanceId: string): Promise<ItemAction[]>;
  call<I, O>(request: ItemCallRequest<I>): Promise<ItemCallResult<O>>;
  subscribe(instanceId: string, listener: (change: ItemChange) => void): () => void;
  bind(placementId: string, binding: WorldBinding): Promise<void>;
  unbind(placementId: string, bindingId: string): Promise<void>;
}

The authoritative module sees a narrow, capability-checked context:

interface ItemRuntimeContext {
  readonly self: { definitionId: string; inventoryInstanceId: string; placementId: string };
  readonly actor: { userId: string; characterId: string } | null;
  readonly state: {
    get<T>(partition: StatePartition, key: string): T;
    patch(partition: StatePartition, values: Record<string, unknown>): void;
  };
  emit(event: string, payload: unknown): void;
  cue(name: string): void;
  callItem(targetInstanceId: string, fn: string, args: unknown): Promise<unknown>;
  character: {
    occupy(socket: string, options?: { animation?: string; channels?: string[] }): void;
    release(): void;
    play(animation: string, options?: { loop?: boolean; layer?: string }): void;
    attach(asset: string, socket: string): void;
  };
  world: {
    invoke(bindingTarget: string, fn: string, payload: unknown): void;
    raycast?(origin: number[], direction: number[], options: { maxDistance: number }): { targetId: string } | null;
    applyDamage?(targetId: string, amount: number, cause: string): void;
    consumeInventory?(inventoryInstanceId: string, quantity: number): Promise<void>;
  };
  now(): number;                    // authority clock
  after(seconds: number, fn: string, args?: unknown): void;
}

interface InteractiveItemModule {
  onSpawn(ctx: ItemRuntimeContext): void | Promise<void>;
  onCall(ctx: ItemRuntimeContext, fn: string, args: unknown): unknown | Promise<unknown>;
  onInput?(ctx: ItemRuntimeContext, frame: ItemInputFrame): void;
  onTick?(ctx: ItemRuntimeContext, dt: number): void;
  onDespawn?(ctx: ItemRuntimeContext): void | Promise<void>;
}

The Unreal plugin exposes the same operations as latent Blueprint/C++ calls and receives the same snapshot/event envelopes. It does not run a separate item protocol.

Character integration

Items never bind keys or manipulate the character controller directly. They request abstract actions and character channels through the first-party ItemInteractionAbility.

  • movement and camera-control are exclusive and priority-arbitrated.
  • upper-body, animation layers, hands, sockets, and input actions are shared.
  • character.occupy("seat") moves the character to an item socket; this is intentionally distinct from attaching a prop to a character bone.
  • Actor clips target helix-humanoid@1, are validated/grounded during publication, and are registered under the item definition/version namespace.
  • A weapon may own aim, upper-body, hands, and fire/reload actions while locomotion continues.
  • A shower or pinball takeover may temporarily own movement and camera until release.

Late placement must be supported: adding an item after the ability manager starts must still register its clips and contribute its animation layer safely.

Authority and security

One command choke point validates every call:

  1. Resolve immutable item definition and instance ownership.
  2. Validate function name and JSON-Schema arguments.
  3. Check auth, world capability grants, and content/runtime compatibility.
  4. Check expected revisions and idempotency.
  5. Enforce depth 8, 200 calls per item per second, declared function limits, memory, and execution-time budgets.
  6. Execute engine-neutral authority logic in a sandbox with no network, filesystem, wall clock, randomness, or host references.
  7. Validate each requested effect against capabilities.
  8. Atomically commit state, idempotency result, and reliable events.
  9. Replicate only fields and events the recipient may read.

Presentation scripts can play cues and simulate non-valuable motion. They cannot write authoritative state, award score, apply damage, consume inventory, grant items, or emit reliable events. Action labels and enabled states are UI hints; the call gate always re-checks authority.

Default-on Vault publication

Universal Item finalization publishes to Vault unless either:

  • the creator preference vaultAutoPublishGeneratedAssets is false; or
  • this publish explicitly sets publication.vault to never.

default means “obey the preference,” not “force public.” The server records whether publication was automatic or explicitly disabled. Current generated-job responses expose vaultAssetId and vaultAutoPublish: "published" | "disabled"; the per-item publication.vault override remains part of this target contract, not a live field.

The deterministic publish transform is:

function toVaultDefinition(source: AuthoringItem): PublishedItemDefinition {
  return {
    definition: canonicalize(source.definition),
    compatibility: source.compatibility,
    assets: pinVaultVersions(source.assets),
    physical: source.physical,
    sockets: source.sockets,
    stateSchema: source.stateSchema,
    stateDefaults: resetToPublishableDefaults(source.stateSchema),
    functions: source.functions,
    events: source.events,
    cues: source.cues,
    character: source.character,
    capabilities: source.capabilities,
    logic: verifiedLogic(source.logic),
    authoringDefaults: pick(source.placementOverrides, source.authoring.portableOverridePaths),
    migrations: source.migrations,
  };
}

It preserves:

  • definition metadata, schema, clean defaults, logic, functions/events, sockets, capabilities;
  • pinned Vault dependencies and runtime renditions;
  • animations, VFX, audio, materials, collision, and compatibility;
  • only explicitly allowlisted portable authoring overrides.

It removes or resets:

  • owned state values, durability, scores, contents, ammunition, preferences, and usage counters;
  • placement IDs, transforms, world IDs, local links, binding IDs, and non-allowlisted overrides;
  • occupants, cooldowns, playback positions, active balls/projectiles, timers, and session handles;
  • owner/user/inventory-instance IDs and acquisition/commerce history;
  • private state, anti-cheat seeds, secrets, credentials, runtime handles, and idempotency records.

Installing the result creates new IDs and revision 0 for every partition. A changed reusable definition becomes a new immutable version or derivative; it never mutates existing pinned worlds.

Runtime adapters

The Instance authority executes one manifest and one logic graph for every runtime. Adapters only translate presentation:

ContractWeb adapterUnreal adapter
GeometryResolve Web GLB renditionResolve cooked Unreal rendition
AnimationRegister helix-humanoid@1 GLB clipsRetarget/use matching UE skeleton clips
VFXMap portable graph to Web rendererMap the same graph to Niagara
AudioWeb Audio spatial sourceUnreal Audio Component
State/eventsShared snapshots, revisions, and event sequenceSame protocol
AuthorityInstance serviceSame Instance service

An engine-specific presentation feature may degrade or be absent. The declared functions, authority rules, state, and reliable events may not diverge.

Example: shower booth

The package owns the booth behavior. A world may bind finished to cleanliness, a quest, or lighting without changing the shower.

{
  "schemaVersion": "1.0",
  "definition": {
    "id": "helix:item:shower-booth",
    "version": "1.0.0",
    "title": "Shower Booth",
    "description": "Portable interactive shower with door, water, temperature and actor poses.",
    "kind": "appliance",
    "implements": ["interactive_item", "appliance.shower"]
  },
  "compatibility": {
    "engines": ["web", "unreal"],
    "systems": ["humanoid-character@^0.2.8"],
    "skeleton": "helix-humanoid@1"
  },
  "assets": {
    "model": { "assetId": "10000000-0000-4000-8000-000000000001", "version": 3, "kind": "prop", "role": "model.primary", "required": true },
    "wash": { "assetId": "10000000-0000-4000-8000-000000000002", "version": 1, "kind": "animation", "role": "animation.wash", "required": true },
    "rinse": { "assetId": "10000000-0000-4000-8000-000000000003", "version": 1, "kind": "animation", "role": "animation.rinse", "required": true },
    "water": { "assetId": "10000000-0000-4000-8000-000000000004", "version": 2, "kind": "audio", "role": "audio.water_loop", "required": true },
    "waterVfx": { "assetId": "10000000-0000-4000-8000-000000000005", "version": 1, "kind": "vfx", "role": "vfx.water", "required": true },
    "doorAudio": { "assetId": "10000000-0000-4000-8000-000000000006", "version": 1, "kind": "audio", "role": "audio.door", "required": true }
  },
  "physical": {
    "sizeMeters": [1.1, 2.18, 1.1],
    "pivot": "footprint-center",
    "collision": { "mode": "bounds" }
  },
  "sockets": {
    "stall": { "type": "stand", "position": [0, 0, 0] },
    "approach": { "type": "stand", "position": [0, 0, 1.1], "rotation": [0, 3.14159, 0] },
    "camera": { "type": "camera", "position": [0, 1.65, 0.2] },
    "showerhead": { "type": "effect", "position": [0, 1.9, -0.1] },
    "door": { "type": "hand", "position": [-0.43, 1.1, 0.49] }
  },
  "state": {
    "owned": {
      "preferredTemp": { "schema": { "type": "number", "minimum": 18, "maximum": 45 }, "default": 38, "authority": "instance", "persistence": "portable", "replication": "owner", "writableBy": "owner" },
      "minutesUsed": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "portable", "replication": "owner", "writableBy": "authority" }
    },
    "placement": {
      "spray": { "schema": { "enum": ["rain", "jet", "mist"] }, "default": "rain", "authority": "instance", "persistence": "placement", "replication": "instance", "writableBy": "world-admin" }
    },
    "session": {
      "doorOpen": { "schema": { "type": "boolean" }, "default": false, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
      "running": { "schema": { "type": "boolean" }, "default": false, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
      "occupant": { "schema": { "type": ["string", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" }
    },
    "private": {}
  },
  "functions": {
    "open": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "optional", "doc": "Open the door when unoccupied." },
    "close": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "optional", "doc": "Close the door." },
    "enter": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "required", "doc": "Occupy the booth." },
    "start": { "input": { "type": "object", "properties": { "temperature": { "type": "number", "minimum": 18, "maximum": 45 } }, "additionalProperties": false }, "auth": "anyone", "mutates": ["owned", "session"], "idempotency": "required", "doc": "Start water and the wash pose." },
    "stop": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "required", "doc": "Stop water and release the actor." },
    "setTemperature": { "input": { "type": "object", "required": ["value"], "properties": { "value": { "type": "number", "minimum": 18, "maximum": 45 } }, "additionalProperties": false }, "auth": "owner", "mutates": ["owned"], "idempotency": "optional", "doc": "Save the owner's preferred temperature." }
  },
  "events": {
    "doorChanged": { "payload": { "type": "object", "required": ["open"], "properties": { "open": { "type": "boolean" } } }, "delivery": "reliable", "audience": "instance" },
    "started": { "payload": { "type": "object", "required": ["actorId"], "properties": { "actorId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" },
    "finished": { "payload": { "type": "object", "required": ["actorId"], "properties": { "actorId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" }
  },
  "cues": {
    "doorOpen": { "itemAnimation": "door.open", "audio": "doorAudio", "at": "door" },
    "doorClose": { "itemAnimation": "door.close", "audio": "doorAudio", "at": "door" },
    "waterOn": { "audio": "water", "vfx": "waterVfx", "at": "showerhead", "loop": true },
    "waterOff": { "audio": "water", "vfx": "waterVfx", "at": "showerhead", "loop": false },
    "washing": { "actorAnimation": "wash", "loop": true }
  },
  "character": {
    "ability": "item-interaction",
    "channels": { "movement": { "priority": 20, "mode": "exclusive" }, "camera-control": { "priority": 20, "mode": "exclusive" } },
    "input": { "cancel": "stop" }
  },
  "capabilities": ["actor.occupy", "actor.animate", "audio.play", "vfx.play"],
  "logic": { "authoritative": { "format": "helix-declarative-v1", "entry": "logic/authority.hlx.json" } },
  "authoring": {
    "portableOverridePaths": ["placement.spray"],
    "worldBindings": [{ "event": "finished", "target": "world", "function": "cleanliness.applyShower" }]
  },
  "publication": { "vault": "default", "primaryAsset": "model" }
}
export const shower: InteractiveItemModule = {
  onSpawn(ctx) {
    if (ctx.state.get('session', 'running')) ctx.state.patch('session', { running: false, occupant: null });
  },
  async onCall(ctx, fn, args: any) {
    const occupant = ctx.state.get<string | null>('session', 'occupant');
    if (fn === 'open' && occupant === null) {
      ctx.state.patch('session', { doorOpen: true });
      ctx.cue('doorOpen');
      ctx.emit('doorChanged', { open: true });
    } else if (fn === 'close') {
      ctx.state.patch('session', { doorOpen: false });
      ctx.cue('doorClose');
      ctx.emit('doorChanged', { open: false });
    } else if (fn === 'enter' && occupant === null && ctx.actor) {
      ctx.state.patch('session', { occupant: ctx.actor.userId, doorOpen: false });
      ctx.character.occupy('stall', { channels: ['movement', 'camera-control'] });
    } else if (fn === 'start' && ctx.actor?.userId === occupant) {
      ctx.state.patch('session', { running: true });
      ctx.state.patch('owned', { preferredTemp: args.temperature });
      ctx.character.play('wash', { loop: true });
      ctx.cue('waterOn');
      ctx.emit('started', { actorId: occupant });
      ctx.after(9, 'stop');
    } else if (fn === 'stop' && (ctx.actor === null || ctx.actor?.userId === occupant)) {
      const used = ctx.state.get<number>('owned', 'minutesUsed');
      ctx.state.patch('owned', { minutesUsed: used + 1 });
      ctx.state.patch('session', { running: false, occupant: null, doorOpen: true });
      ctx.cue('waterOff');
      ctx.character.release();
      ctx.emit('finished', { actorId: occupant });
    } else if (fn === 'setTemperature') {
      ctx.state.patch('owned', { preferredTemp: args.value });
    }
  }
};

Publishing preserves the clean preferredTemp: 38 default and allowlisted spray authoring value. It removes the current owner's preference and usage count, placement transform, occupant, running water, door/session state, world bindings added outside the manifest, and every runtime handle.

Example: networked pinball machine

Physics may be predicted by the playing client, but the Instance authority awards score. Spectators receive replicated ball transforms; the machine's own high-score table is portable owned state.

{
  "schemaVersion": "1.0",
  "definition": {
    "id": "helix:item:pinball-neon",
    "version": "1.0.0",
    "title": "Neon Pinball",
    "description": "Networked pinball machine with per-copy high scores.",
    "kind": "appliance",
    "implements": ["interactive_item", "arcade.pinball"]
  },
  "compatibility": {
    "engines": ["web", "unreal"],
    "systems": ["humanoid-character@^0.2.8"],
    "skeleton": "helix-humanoid@1"
  },
  "assets": {
    "model": { "assetId": "20000000-0000-4000-8000-000000000001", "version": 5, "kind": "prop", "role": "model.primary", "required": true },
    "playPose": { "assetId": "20000000-0000-4000-8000-000000000002", "version": 1, "kind": "animation", "role": "animation.play", "required": true },
    "arcadeAudio": { "assetId": "20000000-0000-4000-8000-000000000003", "version": 2, "kind": "audio", "role": "audio.cues", "required": true },
    "scoreVfx": { "assetId": "20000000-0000-4000-8000-000000000004", "version": 1, "kind": "vfx", "role": "vfx.score", "required": true }
  },
  "physical": { "sizeMeters": [0.8, 1.9, 1.7], "pivot": "footprint-center", "collision": { "mode": "bounds" } },
  "sockets": {
    "player": { "type": "stand", "position": [0, 0, 1.2], "rotation": [0, 3.14159, 0] },
    "camera": { "type": "camera", "position": [0, 1.75, 2.2], "rotation": [0.22, 3.14159, 0] },
    "table": { "type": "effect", "position": [0, 1.0, 0] }
  },
  "state": {
    "owned": {
      "highScores": { "schema": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["userId", "score"], "properties": { "userId": { "type": "string" }, "score": { "type": "integer", "minimum": 0 } } } }, "default": [], "authority": "instance", "persistence": "portable", "replication": "instance", "writableBy": "authority" }
    },
    "placement": {
      "difficulty": { "schema": { "enum": ["easy", "standard", "hard"] }, "default": "standard", "authority": "instance", "persistence": "placement", "replication": "instance", "writableBy": "world-admin" }
    },
    "session": {
      "playerId": { "schema": { "type": ["string", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
      "score": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
      "balls": { "schema": { "type": "integer", "minimum": 0, "maximum": 3 }, "default": 0, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
      "ballPose": { "schema": { "type": ["object", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" }
    },
    "private": {
      "gameEpoch": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "private", "replication": "none", "writableBy": "authority" }
    }
  },
  "functions": {
    "play": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session", "private"], "idempotency": "required", "doc": "Claim the machine and begin a three-ball game." },
    "input": { "input": { "type": "object", "required": ["action", "phase"], "properties": { "action": { "enum": ["leftFlipper", "rightFlipper", "launch", "nudge"] }, "phase": { "enum": ["pressed", "released"] } }, "additionalProperties": false }, "auth": "anyone", "mutates": [], "idempotency": "forbidden", "rateLimit": { "calls": 120, "perSeconds": 1 }, "doc": "Submit transient controls from the current player." },
    "reportBall": { "input": { "type": "object", "required": ["pose"], "properties": { "pose": { "type": "object" } } }, "auth": "anyone", "mutates": ["session"], "idempotency": "forbidden", "rateLimit": { "calls": 30, "perSeconds": 1 }, "doc": "Relay a predicted pose; never awards score." },
    "scoreTarget": { "input": { "type": "object", "required": ["targetId", "gameEpoch"], "properties": { "targetId": { "type": "string" }, "gameEpoch": { "type": "integer", "minimum": 1 } } }, "auth": "authority", "mutates": ["session"], "idempotency": "required", "doc": "Authority-verified target hit." },
    "finishBall": { "input": { "type": "object", "additionalProperties": false }, "auth": "authority", "mutates": ["owned", "session", "private"], "idempotency": "required", "doc": "Advance or end the game and commit a record." },
    "leave": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session", "private"], "idempotency": "required", "doc": "Release the machine." }
  },
  "events": {
    "started": { "payload": { "type": "object", "required": ["playerId"], "properties": { "playerId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" },
    "scored": { "payload": { "type": "object", "required": ["score"], "properties": { "score": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" },
    "gameOver": { "payload": { "type": "object", "required": ["score"], "properties": { "score": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" },
    "newRecord": { "payload": { "type": "object", "required": ["score"], "properties": { "score": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" }
  },
  "cues": {
    "playing": { "actorAnimation": "playPose", "loop": true },
    "score": { "audio": "arcadeAudio", "vfx": "scoreVfx", "at": "table" }
  },
  "character": {
    "ability": "item-interaction",
    "channels": { "movement": { "priority": 20, "mode": "exclusive" }, "camera-control": { "priority": 20, "mode": "exclusive" } },
    "input": { "look": "aim", "primary": "leftFlipper", "secondary": "rightFlipper", "tertiary": "launch", "cancel": "leave" }
  },
  "capabilities": ["actor.occupy", "actor.animate", "physics.predict", "audio.play", "vfx.play"],
  "logic": { "authoritative": { "format": "helix-declarative-v1", "entry": "logic/authority.hlx.json" } },
  "authoring": {
    "portableOverridePaths": ["placement.difficulty"],
    "worldBindings": [{ "event": "newRecord", "target": "world", "function": "arcade.onRecord" }]
  },
  "publication": { "vault": "default", "primaryAsset": "model" }
}
export const pinball: InteractiveItemModule = {
  onSpawn(ctx) {
    ctx.state.patch('session', { playerId: null, score: 0, balls: 0, ballPose: null });
    ctx.state.patch('private', { gameEpoch: 0 });
  },
  async onCall(ctx, fn, args: any) {
    const playerId = ctx.state.get<string | null>('session', 'playerId');
    if (fn === 'play' && playerId === null && ctx.actor) {
      const gameEpoch = ctx.state.get<number>('private', 'gameEpoch') + 1;
      ctx.state.patch('session', { playerId: ctx.actor.userId, score: 0, balls: 3 });
      ctx.state.patch('private', { gameEpoch });
      ctx.character.occupy('player', { animation: 'playPose', channels: ['movement', 'camera-control'] });
      ctx.emit('started', { playerId: ctx.actor.userId });
    } else if (fn === 'input' && ctx.actor?.userId === playerId) {
      ctx.world.invoke('physics', 'pinball.input', args);
    } else if (fn === 'reportBall' && ctx.actor?.userId === playerId) {
      ctx.state.patch('session', { ballPose: args.pose });
    } else if (fn === 'scoreTarget') {
      if (args.gameEpoch !== ctx.state.get<number>('private', 'gameEpoch')) return;
      const points: Record<string, number> = { bumper: 10, ramp: 25, jackpot: 100 };
      const score = ctx.state.get<number>('session', 'score') + (points[args.targetId] ?? 0);
      ctx.state.patch('session', { score });
      ctx.cue('score');
      ctx.emit('scored', { score });
    } else if (fn === 'finishBall') {
      const balls = ctx.state.get<number>('session', 'balls');
      if (balls > 1) {
        ctx.state.patch('session', { balls: balls - 1, ballPose: null });
        return;
      }
      const score = ctx.state.get<number>('session', 'score');
      const highScores = ctx.state.get<Array<{ userId: string; score: number }>>('owned', 'highScores');
      const next = [...highScores, { userId: playerId!, score }]
        .sort((a, b) => b.score - a.score).slice(0, 10);
      const record = next[0]?.userId === playerId && next[0]?.score === score;
      ctx.state.patch('owned', { highScores: next });
      ctx.state.patch('session', { playerId: null, balls: 0, ballPose: null });
      ctx.emit('gameOver', { score });
      if (record) ctx.emit('newRecord', { score });
      ctx.character.release();
    } else if (fn === 'leave' && ctx.actor?.userId === playerId) {
      ctx.state.patch('session', { playerId: null, ballPose: null });
      ctx.character.release();
    }
  }
};

Publishing keeps an empty highScores default and the allowlisted difficulty. It removes the actual leaderboard from the source copy, player, score, balls, ball pose, nonce, camera takeover, transform, and world-added bindings. A platform-wide leaderboard is a separate service, not owned-item state.

Example: universal weapon

The weapon composes with locomotion through upper-body/hands channels. Damage and ammunition are authoritative world/platform operations; recoil, muzzle flash, shell motion, and hit markers are presentation.

{
  "schemaVersion": "1.0",
  "definition": {
    "id": "helix:item:carbine-mk1",
    "version": "1.0.0",
    "title": "Carbine MK1",
    "description": "Portable ranged weapon with shared equip, aim, fire and reload behavior.",
    "kind": "weapon",
    "implements": ["interactive_item", "weapon.ranged"]
  },
  "compatibility": {
    "engines": ["web", "unreal"],
    "systems": ["humanoid-character@^0.2.8"],
    "skeleton": "helix-humanoid@1"
  },
  "assets": {
    "model": { "assetId": "30000000-0000-4000-8000-000000000001", "version": 4, "kind": "prop", "role": "model.primary", "required": true },
    "aim": { "assetId": "30000000-0000-4000-8000-000000000002", "version": 1, "kind": "animation", "role": "animation.aim", "required": true },
    "fire": { "assetId": "30000000-0000-4000-8000-000000000003", "version": 1, "kind": "animation", "role": "animation.fire", "required": true },
    "reload": { "assetId": "30000000-0000-4000-8000-000000000004", "version": 2, "kind": "animation", "role": "animation.reload", "required": true },
    "shot": { "assetId": "30000000-0000-4000-8000-000000000005", "version": 1, "kind": "audio", "role": "audio.fire", "required": true },
    "reloadAudio": { "assetId": "30000000-0000-4000-8000-000000000006", "version": 1, "kind": "audio", "role": "audio.reload", "required": true },
    "muzzleFlash": { "assetId": "30000000-0000-4000-8000-000000000007", "version": 1, "kind": "vfx", "role": "vfx.muzzle", "required": true }
  },
  "physical": { "sizeMeters": [0.18, 0.34, 0.86], "pivot": "footprint-center", "collision": { "mode": "bounds" } },
  "sockets": {
    "grip": { "type": "hand", "position": [0, 0, 0], "node": "Grip_R" },
    "support": { "type": "hand", "position": [0, 0, 0], "node": "Grip_L" },
    "muzzle": { "type": "effect", "position": [0, 0.05, -0.83], "node": "Muzzle" }
  },
  "state": {
    "owned": {
      "durability": { "schema": { "type": "number", "minimum": 0, "maximum": 100 }, "default": 100, "authority": "platform", "persistence": "portable", "replication": "owner", "writableBy": "authority" },
      "finish": { "schema": { "type": "string", "maxLength": 64 }, "default": "factory", "authority": "instance", "persistence": "portable", "replication": "instance", "writableBy": "owner" }
    },
    "placement": {},
    "session": {
      "wielderId": { "schema": { "type": ["string", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
      "magazine": { "schema": { "type": "integer", "minimum": 0, "maximum": 30 }, "default": 30, "authority": "instance", "persistence": "transient", "replication": "owner", "writableBy": "authority" },
      "reserve": { "schema": { "type": "integer", "minimum": 0, "maximum": 180 }, "default": 90, "authority": "instance", "persistence": "transient", "replication": "owner", "writableBy": "authority" },
      "cooldownUntil": { "schema": { "type": "number", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "transient", "replication": "owner", "writableBy": "authority" }
    },
    "private": {
      "shotSequence": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "private", "replication": "none", "writableBy": "authority" }
    }
  },
  "functions": {
    "equip": { "input": { "type": "object", "additionalProperties": false }, "auth": "owner", "mutates": ["session"], "idempotency": "required", "doc": "Attach and activate weapon channels." },
    "unequip": { "input": { "type": "object", "additionalProperties": false }, "auth": "owner", "mutates": ["session"], "idempotency": "required", "doc": "Release channels and attachment." },
    "aim": { "input": { "type": "object", "required": ["active"], "properties": { "active": { "type": "boolean" } }, "additionalProperties": false }, "auth": "owner", "mutates": [], "idempotency": "forbidden", "doc": "Set aim presentation state." },
    "fire": { "input": { "type": "object", "required": ["origin", "direction"], "properties": { "origin": { "type": "array", "minItems": 3, "maxItems": 3 }, "direction": { "type": "array", "minItems": 3, "maxItems": 3 } }, "additionalProperties": false }, "auth": "owner", "mutates": ["owned", "session", "private"], "idempotency": "required", "rateLimit": { "calls": 10, "perSeconds": 1 }, "doc": "Validate a shot, consume ammunition, and apply an authoritative hit." },
    "reload": { "input": { "type": "object", "additionalProperties": false }, "auth": "owner", "mutates": ["session"], "idempotency": "required", "doc": "Move reserve ammunition into the magazine." }
  },
  "events": {
    "equipped": { "payload": { "type": "object", "required": ["actorId"], "properties": { "actorId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" },
    "fired": { "payload": { "type": "object", "required": ["actorId", "sequence"], "properties": { "actorId": { "type": "string" }, "sequence": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" },
    "hit": { "payload": { "type": "object", "required": ["targetId", "damage"], "properties": { "targetId": { "type": "string" }, "damage": { "type": "number" } } }, "delivery": "reliable", "audience": "instance" },
    "depleted": { "payload": { "type": "object", "additionalProperties": false }, "delivery": "reliable", "audience": "owner" }
  },
  "cues": {
    "aim": { "actorAnimation": "aim", "loop": true },
    "fire": { "actorAnimation": "fire", "audio": "shot", "vfx": "muzzleFlash", "at": "muzzle" },
    "reload": { "actorAnimation": "reload", "audio": "reloadAudio" }
  },
  "character": {
    "ability": "item-interaction",
    "channels": {
      "upper-body": { "priority": 30, "mode": "shared" },
      "hands": { "priority": 30, "mode": "shared" },
      "camera-control": { "priority": 10, "mode": "shared" }
    },
    "input": { "look": "aim", "primary": "fire", "secondary": "aim", "tertiary": "reload" }
  },
  "capabilities": ["actor.animate", "actor.attach", "combat.raycast", "combat.damage", "audio.play", "vfx.play"],
  "logic": { "authoritative": { "format": "helix-declarative-v1", "entry": "logic/authority.hlx.json" } },
  "authoring": {
    "portableOverridePaths": ["owned.finish"],
    "worldBindings": [{ "event": "hit", "target": "world", "function": "combat.onWeaponHit" }]
  },
  "publication": { "vault": "default", "primaryAsset": "model" }
}
export const weapon: InteractiveItemModule = {
  onSpawn(ctx) {
    ctx.state.patch('session', { wielderId: null, magazine: 30, reserve: 90, cooldownUntil: 0 });
    ctx.state.patch('private', { shotSequence: 0 });
  },
  async onCall(ctx, fn, args: any) {
    const wielderId = ctx.state.get<string | null>('session', 'wielderId');
    if (fn === 'equip' && ctx.actor) {
      ctx.state.patch('session', { wielderId: ctx.actor.userId });
      ctx.character.attach('model', 'hand_r.grip');
      ctx.emit('equipped', { actorId: ctx.actor.userId });
    } else if (fn === 'fire' && ctx.actor?.userId === wielderId) {
      const now = ctx.now();
      const magazine = ctx.state.get<number>('session', 'magazine');
      if (magazine <= 0) return ctx.emit('depleted', {});
      if (now < ctx.state.get<number>('session', 'cooldownUntil')) return;
      const sequence = ctx.state.get<number>('private', 'shotSequence') + 1;
      const durability = ctx.state.get<number>('owned', 'durability');
      ctx.state.patch('session', { magazine: magazine - 1, cooldownUntil: now + 100 });
      ctx.state.patch('owned', { durability: Math.max(0, durability - 0.02) });
      ctx.state.patch('private', { shotSequence: sequence });
      const hit = ctx.world.raycast?.(args.origin, args.direction, { maxDistance: 250 });
      if (hit) {
        ctx.world.applyDamage?.(hit.targetId, 20, ctx.self.definitionId);
        ctx.emit('hit', { targetId: hit.targetId, damage: 20 });
      }
      ctx.cue('fire');
      ctx.emit('fired', { actorId: wielderId, sequence });
    } else if (fn === 'aim' && ctx.actor?.userId === wielderId) {
      if (args.active) ctx.character.play('aim', { loop: true, layer: 'upper-body' });
    } else if (fn === 'reload' && ctx.actor?.userId === wielderId) {
      const magazine = ctx.state.get<number>('session', 'magazine');
      const reserve = ctx.state.get<number>('session', 'reserve');
      const moved = Math.min(30 - magazine, reserve);
      ctx.state.patch('session', { magazine: magazine + moved, reserve: reserve - moved });
      ctx.cue('reload');
    } else if (fn === 'unequip' && ctx.actor?.userId === wielderId) {
      ctx.state.patch('session', { wielderId: null });
      ctx.character.release();
    }
  }
};

Publishing keeps durability/finish schema and clean defaults, but removes the owner's current durability/finish unless the finish is deliberately passed as an allowlisted derivative override. It always removes ammunition, cooldown, wielder, shot sequence, hit data, runtime attachments, and world combat bindings added outside the package.

Migration and compatibility

  • Definitions and Vault versions are immutable. Editing creates a new version.
  • Patch versions may change presentation without changing state/function/event schemas.
  • Minor versions may add optional fields, functions, or events.
  • Major versions require deterministic migrations and cannot silently upgrade pinned World Builds.
  • Migrations receive only the old validated state document and declared defaults. They have no I/O, clock, randomness, user lookup, or runtime context.
  • Migration is atomic: validate source → transform → validate target → commit all partitions and revisions, or keep the old version untouched.
  • An unsupported runtime rendition fails installation before the world starts; it never falls back silently to an incompatible asset.

Ordered implementation and conformance plan

  1. Publish [email protected] types, JSON Schema, validator, canonicalizer, package reader/writer, and golden fixtures for these three examples.
  2. Add the item definition version and pinned Vault related-artifact records without duplicating media blobs.
  3. Add owned/placement/session/private revision envelopes and idempotent item call storage.
  4. Implement the authoritative command service, capability gate, replication filter, reliable event sequence, call-depth/rate/memory/time budgets, and deterministic timers.
  5. Port the proven declarative interpreter; reject authority scripts for cross-engine definitions.
  6. Ship ItemInteractionAbility, late clip/layer registration, sockets, abstract input, and channel arbitration in the character runtime.
  7. Implement Web and Unreal adapters against the same recorded protocol fixtures.
  8. Make Universal Item finalization run the exact Vault publish transform by default, with preference and per-publish opt-out tests.
  9. Add SDK/CLI/MCP operations for inspect, place, configure, bind, call, validate, publish, install, upgrade, and usage tracking; MCP remains an instruction layer over the CLI.
  10. Run a two-client, two-runtime conformance suite: publish each example, install it into another creator's world, exercise functions/events/state/replication, reconnect, transfer/replace, migrate, republish, and prove source state did not leak.

The acceptance test is simple: one creator publishes an interactive object, another creator or agent places it in a different world, and its standard behavior survives while its typed events and functions safely participate in that world's custom logic.

Source map

Current claims in this handoff are grounded in:

  • Backend owned state: helix-backend-api/src/database/entities/user-inventory-item.entity.ts
  • Backend placement state: helix-backend-api/src/database/entities/home-item-placement.entity.ts
  • Product-to-Vault reference: helix-backend-api/src/database/entities/universal-item.entity.ts
  • Default-on preference: helix-backend-api/src/database/entities/user.entity.ts
  • Vault resources/versions/contracts: helix-backend-api/src/vault and src/database/entities/vault-asset*.ts
  • Character abilities/channels/sockets: helix-web-engine-new/docs/character-architecture.md
  • Proven item manifest/runtime: helixgame.com/prototypes/interactive-items/src/runtime
  • Proven examples and character bridge findings: helixgame.com/prototypes/interactive-items/items and SPEC-CHANGES.md

On this page