Loading...
Loading...
Cross-cutting runtime APIs for Decentraland SDK7 scenes. Use when the user needs async work (executeTask), HTTP (fetch/signedFetch) or WebSocket, timers, realm/scene metadata, restricted actions (movePlayerTo, teleport, emotes, external URLs), system execution order/priority, or to write scene tests. Do NOT use for UI (see build-ui), multiplayer sync (see multiplayer-sync), avatar/player data (see player-avatar), or polling-based input (see advanced-input).
npx skill4agent add decentraland/sdk-skills scene-runtimeexecuteTask()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);
});const res = await fetch("https://api.example.com/data");getHeaders()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" });signedFetch{ ok, status, statusText, headers, body }bodyJSON.parse(response.body).json()Permission: the nominal permission for external HTTP is(and"USE_FETCH"for sockets), declared in"USE_WEBSOCKET"scene.json. In practice plain/signedrequiredPermissionsto non-media hosts is not hard-blocked in preview/Worlds even without it (thefetchtest scene calls66,6-signed-fetchwith an emptysignedFetch). DeclarerequiredPermissionsanyway for correctness and forward-compat.USE_FETCHdoes not require prior player interaction — restricted actions do,signedFetch/fetchdo not.signedFetch
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, getExplorerInformation } from "~system/Runtime";
executeTask(async () => {
// Scene info: URN, content mappings, metadata JSON, baseUrl
const scene = await getSceneInformation({});
const metadata = JSON.parse(scene.metadataJson);
// metadata.scene?.parcels (parcel list), metadata.display?.title (scene title)
console.log(scene.urn, scene.baseUrl, metadata);
// Realm info: baseUrl, realmName, isPreview, networkId (1 = mainnet, 5 = goerli), commsAdapter
const realm = await getRealm({});
console.log(realm.realmInfo?.realmName, realm.realmInfo?.isPreview); // realmName e.g. "peer-us-1"
// Explorer info: agent string, platform, configurations
const explorer = await getExplorerInformation({});
console.log(explorer.agent, explorer.platform);
});realm.realmInfo?.isPreviewimport { getWorldTime } from "~system/Runtime";
executeTask(async () => {
const { seconds } = await getWorldTime({});
// seconds = coordinated world time (cycles 0-86400 for day/night)
});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);
});import { EngineInfo } from "@dcl/sdk/ecs";
engine.addSystem(() => {
const info = EngineInfo.getOrNull(engine.RootEntity);
if (info) {
console.log(info.frameNumber, info.tickNumber, info.totalRuntime);
}
});engine.addSystem(fn, priority?, name?)fn(dt)prioritysort((a, b) => b.priority - a.priority)@dcl/ecsWARNING — counter-intuitive: This is the OPPOSITE of Unity/Godot/many engines where a lower number runs first. In Decentraland SDK7, "make this run first" means giving it a large priority number, NOT. A system with priority1runs almost LAST.1
100000SYSTEMS_REGULAR_PRIORITY = 100e3engine.addSystem(fn)@dcl/react-ecs100000100001100000engine.addSystem(fn, 1000000)100000100engine.addSystem(earlySystem, 1000000); // runs before regular systems
engine.addSystem(regularSystem); // priority 100000 (default)
engine.addSystem(lateSystem, 10); // runs after regular systemsupdate(dt)~system/RestrictedActionsimport {
movePlayerTo,
teleportTo,
triggerEmote,
changeRealm,
openExternalUrl,
openNftDialog,
triggerSceneEmote,
copyToClipboard,
setCommunicationsAdapter,
} from "~system/RestrictedActions";
// Move player within scene bounds. Optional: cameraTarget (where the
// CAMERA looks), avatarTarget (where the AVATAR faces — for rotating in
// place), duration (seconds, for a smooth glide instead of a snap).
movePlayerTo({
newRelativePosition: { x: 8, y: 0, z: 8 },
cameraTarget: { x: 8, y: 1, z: 12 },
});
// 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({ text: "Hello from Decentraland!" });
// Change realm. `message` is OPTIONAL: omit it to switch with no prompt,
// include it to show the player a confirmation dialog first.
changeRealm({ realm: "https://peer.decentraland.org" }); // no prompt
changeRealm({ realm: "other-realm.dcl.eth", message: "Join this realm?" });timers@dcl/sdk/ecssetTimeoutsetIntervalsetTimeoutclearTimeoutsetIntervalclearInterval@dcl/js-runtime/index.d.tstimers.setTimeoutimport { timers } from "@dcl/sdk/ecs";
const timeoutId = timers.setTimeout(() => console.log("delayed"), 2000);
timers.clearTimeout(timeoutId);
const intervalId = timers.setInterval(() => console.log("tick"), 1000);
timers.clearInterval(intervalId);timers.setTimeout(callback: () => void, ms: number): number
timers.clearTimeout(timerId: number): void
timers.setInterval(callback: () => void, ms: number): number
timers.clearInterval(timerId: number): void(callback, ms)(ms, callback)dttimerscreateTimers(engineInstance)@dcl/sdk/ecsTimerslet elapsed = 0;
engine.addSystem((dt: number) => {
elapsed += dt;
if (elapsed >= 3) {
elapsed = 0;
// Do something every 3 seconds
}
});Transform.onChange(engine.PlayerEntity, (newValue) => {
if (newValue) {
console.log("Player moved to", newValue.position);
}
});import { removeEntityWithChildren } from "@dcl/sdk/ecs";
removeEntityWithChildren(engine, parentEntity);~system/PortableExperiencesimport {
spawn,
kill,
exit,
getPortableExperiencesLoaded,
} from "~system/PortableExperiences";
// Spawn by ENS name (a deployed World) OR by pid. NOT by "urn".
const result = await spawn({ ens: "boedo.dcl.eth" });
// result: { pid, parentCid, name, ens }
// Kill a running one by its pid (from the spawn response). NOT by urn.
if (result.pid) await kill({ pid: result.pid });
// List currently loaded portable experiences
const { loaded } = await getPortableExperiencesLoaded({});
// Exit self (only if THIS scene IS a portable experience)
await exit({});spawn({ ens?, pid? })SpawnResponse { pid, parentCid, name, ens? }enspidurnkill({ pid }){ status: boolean }kill({ pid })getPortableExperiencesLoaded({})pidurnscene.json"featureToggles": { "portableExperiences": "enabled" }"enabled""disabled""hideUi""disabled"spawn()@dcl/sdk/testingimport { test } from "@dcl/sdk/testing";
import {
assertComponentValue,
assertEquals,
} from "@dcl/sdk/testing/assert";
import { engine, Transform, MeshRenderer } from "@dcl/sdk/ecs";
import { Vector3, Quaternion } from "@dcl/sdk/math";
test("transform is applied after one frame", function* () {
const entity = engine.addEntity();
Transform.create(entity, { position: Vector3.One() });
// Let the engine run for a frame before asserting
yield;
assertComponentValue(entity, Transform, {
position: Vector3.One(),
scale: Vector3.One(),
rotation: Quaternion.Identity(),
parent: 0 as any,
});
});
test("five meshes are present", function* () {
yield;
assertEquals(1 + 1, 2, "basic math");
// No count assertion exists — count via getEntitiesWith + Array.from
assertEquals(
Array.from(engine.getEntitiesWith(MeshRenderer)).length,
5,
"should have 5 meshes"
);
});@dcl/sdk/testing/assertassertEquals(actual, expected, message?)assert(condition, message?)assertComponentValue(entity, Component, expected)deepCloseTo(actual, expected, options?)assertEquals(Array.from(engine.getEntitiesWith(Comp)).length, n)npx @dcl/sdk-commands test~system/Testingconsole.log()console.error()console.warn().info().debug().trace()signedFetchresponse.ok.status.bodymovePlayerTocameraTargetavatarTargetteleportTotriggerEmotetriggerSceneEmoteopenExternalUrlopenNftDialogchangeRealmmessagespawn({ ens })kill({ pid })scene.jsonportableExperiences: "enabled"scene.jsonportableExperiences: "disabled"scene.jsonportableExperiences: "hideUi"triggerSceneEmote_emote.glbsetCommunicationsAdaptermovePlayerToexecuteTask{baseDir}/references/runtime-apis.md