skills/scene-runtime/SKILL.md
Cross-cutting runtime APIs for Decentraland SDK7 scenes. Use when the user needs async operations (executeTask), HTTP requests (fetch, signedFetch), WebSocket connections, timers, realm/scene detection, restricted actions (movePlayerTo, teleportTo, triggerEmote, openExternalUrl), portable experiences, or the testing framework. Do NOT use for UI (see build-ui), multiplayer sync (see multiplayer-sync), or avatar/player data (see player-avatar).
npx skillsauth add dcl-regenesislabs/opendcl scene-runtimeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Cross-cutting runtime APIs available in every Decentraland SDK7 scene.
The scene runtime is single-threaded. Wrap any async work in executeTask():
import { executeTask } from '@dcl/sdk/ecs'
executeTask(async () => {
const res = await fetch('https://api.example.com/data')
const data = await res.json()
console.log(data)
})
Plain fetch works for public APIs:
const res = await fetch('https://api.example.com/data')
signedFetch proves the player's identity to your backend. Use getHeaders() to obtain only the signed headers (useful when a library manages its own fetch):
import { signedFetch, getHeaders } from '~system/SignedFetch'
// Full signed request
const res = await signedFetch({ url: 'https://your-server.com/api', init: { method: 'POST', body: JSON.stringify(payload) } })
// Get signed headers only (for custom fetch calls)
const { headers } = await getHeaders({ url: 'https://your-server.com/api' })
Permission: External HTTP requires
"ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE"or no special permission for plain fetch;signedFetchneeds the player to have interacted with the scene.
const ws = new WebSocket('wss://your-server.com/ws')
ws.onopen = () => ws.send('hello')
ws.onmessage = (event) => console.log(event.data)
ws.onclose = () => console.log('disconnected')
import { getSceneInformation, getRealm } from '~system/Runtime'
import { getExplorerInformation } from '~system/EnvironmentApi'
executeTask(async () => {
// Scene info: URN, content mappings, metadata JSON, baseUrl
const scene = await getSceneInformation({})
const metadata = JSON.parse(scene.metadataJson)
console.log(scene.urn, scene.baseUrl, metadata)
// Realm info: baseUrl, realmName, isPreview, networkId, commsAdapter
const realm = await getRealm({})
console.log(realm.realmInfo?.realmName, realm.realmInfo?.isPreview)
// Explorer info: agent string, platform, configurations
const explorer = await getExplorerInformation({})
console.log(explorer.agent, explorer.platform)
})
import { getWorldTime } from '~system/Runtime'
executeTask(async () => {
const { seconds } = await getWorldTime({})
// seconds = coordinated world time (cycles 0-86400 for day/night)
})
Read files deployed with the scene at runtime:
import { readFile } from '~system/Runtime'
executeTask(async () => {
const result = await readFile({ fileName: 'data/config.json' })
const text = new TextDecoder().decode(result.content)
const config = JSON.parse(text)
})
Access frame-level timing:
import { EngineInfo } from '@dcl/sdk/ecs'
engine.addSystem(() => {
const info = EngineInfo.getOrNull(engine.RootEntity)
if (info) {
console.log(info.frameNumber, info.tickNumber, info.totalRuntime)
}
})
These require player interaction before they can execute. Import from ~system/RestrictedActions:
import {
movePlayerTo,
teleportTo,
triggerEmote,
changeRealm,
openExternalUrl,
openNftDialog,
triggerSceneEmote,
copyToClipboard,
setCommunicationsAdapter
} from '~system/RestrictedActions'
// Move player within scene bounds
movePlayerTo({ newRelativePosition: { x: 8, y: 0, z: 8 } })
// Teleport to coordinates in Genesis City
teleportTo({ worldCoordinates: { x: 50, y: 70 } })
// Play a built-in emote
triggerEmote({ predefinedEmote: 'wave' })
// Open URL in browser (prompts user)
openExternalUrl({ url: 'https://decentraland.org' })
// Open NFT detail dialog
openNftDialog({ urn: 'urn:decentraland:ethereum:erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d:558536' })
// Copy text to clipboard
copyToClipboard({ value: 'Hello from Decentraland!' })
// Change realm
changeRealm({ realm: 'other-realm.dcl.eth', message: 'Join this realm?' })
Use the timers API from @dcl/sdk/ecs, not the global setTimeout/setInterval. The globals are not reliable in the QuickJS runtime.
import { timers } from '@dcl/sdk/ecs'
const t = timers.setTimeout(() => console.log('delayed'), 2000)
timers.clearTimeout(t)
const id = timers.setInterval(() => console.log('tick'), 1000)
timers.clearInterval(id)
System-based timers (recommended for game logic — synchronized with the frame loop):
let elapsed = 0
engine.addSystem((dt: number) => {
elapsed += dt
if (elapsed >= 3) {
elapsed = 0
// Do something every 3 seconds
}
})
React to component changes on any entity:
Transform.onChange(engine.PlayerEntity, (newValue) => {
if (newValue) {
console.log('Player moved to', newValue.position)
}
})
Recursively remove an entity and all its children:
import { removeEntityWithChildren } from '@dcl/sdk/ecs'
removeEntityWithChildren(engine, parentEntity)
Scenes that persist across world navigation:
import { spawn, kill, exit, getPortableExperiencesLoaded } from '~system/PortableExperiences'
// Spawn a portable experience by URN
const result = await spawn({ urn: 'urn:decentraland:entity:bafk...' })
// List currently loaded portable experiences
const loaded = await getPortableExperiencesLoaded({})
// Kill a specific portable experience
await kill({ urn: 'urn:decentraland:entity:bafk...' })
// Exit self (if this scene IS a portable experience)
await exit({})
SDK7 includes a testing framework for automated scene tests:
import { test, assert, assertEquals, assertComponentValue } from '@dcl/sdk/testing'
import { setCameraTransform } from '@dcl/sdk/testing'
test('cube is at correct position', async (context) => {
// Set up camera for the test
setCameraTransform({ position: { x: 8, y: 1, z: 8 } })
// Wait for systems to run
await context.helpers.waitNTicks(2)
// Assert component values
assertComponentValue(cubeEntity, Transform, {
position: Vector3.create(8, 1, 8)
})
// Basic assertions
assert(Transform.has(cubeEntity), 'Entity should have Transform')
assertEquals(1 + 1, 2)
})
Run tests with:
npx @dcl/sdk-commands test
executeTask() — bare promises will be silently droppedsignedFetch (not plain fetch) when your backend needs to verify the player's identitytimers.setTimeout/timers.setInterval for game logic — they stay in sync with the frame loop. Use timers.* for one-shot scheduled actions (auto-close door, delayed sound, etc.).realm.realmInfo?.isPreview to detect preview mode and enable debug featuresreadFile() for data files (JSON configs, level data) deployed alongside the sceneremoveEntityWithChildren() is essential when cleaning up complex entity hierarchiesFor complete executeTask patterns, all RestrictedActions, realm detection, and portable experiences, see {baseDir}/references/runtime-apis.md.
development
Capture screenshots of the running Decentraland preview to verify scene changes visually. Covers camera movement, interaction actions, and visual debugging. Use when the preview is running and you need to check what the scene looks like, debug visual issues, or verify layout. Do NOT use for code changes (make changes first, then screenshot).
development
Apply physics forces to the player in Decentraland scenes. Impulses (one-shot pushes), knockback (push away from a point with falloff), continuous forces (wind tunnels, anti-gravity, lift, levitation, hover), timed forces, and repulsion fields. Use when the user wants launch pads, knockback on hit, wind zones, gravity fields, jumps, lifting/floating the player, pushing the player up/sideways/back, hover effects, or any scene-applied force on the player. THIS is also the right skill when an agent's first instinct is to mutate `Transform` on `engine.PlayerEntity` to move/lift/push the player — that does NOT work (the player Transform is engine-controlled and read-only); use the Physics API instead. Do NOT use for player movement speed (see player-avatar AvatarLocomotionSettings) or platform movement (see animations-tweens).
data-ai
Player and avatar system in Decentraland. Read player position/profile, customize appearance (AvatarBase), trigger emotes (triggerEmote/triggerSceneEmote), read equipped wearables (AvatarEquippedData), attach objects to players (AvatarAttach), create NPC avatars (AvatarShape), avatar modifier areas, and locomotion settings. Use when the user wants player data, emotes, wearables, NPC avatars, avatar attachments, or movement speed changes. Do NOT use for wallet/blockchain interactions (see nft-blockchain).
development
Emit particles (fire, smoke, sparks, snow, magic, fireworks) from an entity in a Decentraland SDK7 scene with the ParticleSystem component. Covers emitter shapes (Point, Sphere, Cone, Box), continuous rate vs Burst emission, lifetime/size/color/velocity ranges, gravity and additionalForce, blend modes (ALPHA/ADD/MULTIPLY), billboard and faceTravelDirection, sprite-sheet texture animation, simulation space (local vs world), playback state, and per-scene particle budget. Use when the user asks for particles, sparks, fire, smoke, dust, fog, fireworks, magic effects, snowfall, rain, embers, trails, or atmospheric effects. Do NOT use for procedural entity motion (see animations-tweens), GLTF model effects (see add-3d-models), or 2D UI effects (see build-ui).