| name | scene-runtime |
| description | 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). |
Scene Runtime APIs
Cross-cutting runtime APIs available in every Decentraland SDK7 scene.
Async Tasks
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)
})
HTTP: fetch & signedFetch
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'
const res = await signedFetch({ url: 'https://your-server.com/api', init: { method: 'POST', body: JSON.stringify(payload) } })
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; signedFetch needs the player to have interacted with the scene.
WebSocket
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')
Scene & Realm Information
import { getSceneInformation, getRealm } from '~system/Runtime'
import { getExplorerInformation } from '~system/EnvironmentApi'
executeTask(async () => {
const scene = await getSceneInformation({})
const metadata = JSON.parse(scene.metadataJson)
console.log(scene.urn, scene.baseUrl, metadata)
const realm = await getRealm({})
console.log(realm.realmInfo?.realmName, realm.realmInfo?.isPreview)
const explorer = await getExplorerInformation({})
console.log(explorer.agent, explorer.platform)
})
World Time
import { getWorldTime } from '~system/Runtime'
executeTask(async () => {
const { seconds } = await getWorldTime({})
})
Read Deployed Files
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)
})
EngineInfo Component
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)
}
})
Restricted Actions
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'
movePlayerTo({ newRelativePosition: { x: 8, y: 0, z: 8 } })
teleportTo({ worldCoordinates: { x: 50, y: 70 } })
triggerEmote({ predefinedEmote: 'wave' })
openExternalUrl({ url: 'https://decentraland.org' })
openNftDialog({ urn: 'urn:decentraland:ethereum:erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d:558536' })
copyToClipboard({ value: 'Hello from Decentraland!' })
changeRealm({ realm: 'other-realm.dcl.eth', message: 'Join this realm?' })
Timers
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
}
})
Component.onChange() Listener
React to component changes on any entity:
Transform.onChange(engine.PlayerEntity, (newValue) => {
if (newValue) {
console.log('Player moved to', newValue.position)
}
})
Utility: removeEntityWithChildren
Recursively remove an entity and all its children:
import { removeEntityWithChildren } from '@dcl/sdk/ecs'
removeEntityWithChildren(engine, parentEntity)
Portable Experiences
Scenes that persist across world navigation:
import { spawn, kill, exit, getPortableExperiencesLoaded } from '~system/PortableExperiences'
const result = await spawn({ urn: 'urn:decentraland:entity:bafk...' })
const loaded = await getPortableExperiencesLoaded({})
await kill({ urn: 'urn:decentraland:entity:bafk...' })
await exit({})
Testing Framework
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) => {
setCameraTransform({ position: { x: 8, y: 1, z: 8 } })
await context.helpers.waitNTicks(2)
assertComponentValue(cubeEntity, Transform, {
position: Vector3.create(8, 1, 8)
})
assert(Transform.has(cubeEntity), 'Entity should have Transform')
assertEquals(1 + 1, 2)
})
Run tests with:
npx @dcl/sdk-commands test
Best Practices
- Always wrap async code in
executeTask() — bare promises will be silently dropped
- Use
signedFetch (not plain fetch) when your backend needs to verify the player's identity
- Prefer system-based timers over
timers.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.).
- Check
realm.realmInfo?.isPreview to detect preview mode and enable debug features
- Use
readFile() for data files (JSON configs, level data) deployed alongside the scene
removeEntityWithChildren() is essential when cleaning up complex entity hierarchies
For complete executeTask patterns, all RestrictedActions, realm detection, and portable experiences, see {baseDir}/references/runtime-apis.md.